A competitor list tells you who you already know. A SERP market map can show which types of sites repeatedly control discovery across a topic: product companies, publishers, directories, communities, marketplaces, or review sites.
The useful output is not another ranking export. It is a table that answers:
- Which domains appear across multiple related queries?
- Which domain categories occupy the most search visibility?
- Which sites rank broadly, and which only own one narrow intent?
- Where is the market crowded with direct vendors, and where do intermediaries dominate?
This walkthrough builds that output from several SERPs, rather than treating one keyword as the whole market.
1. Define a query set around buyer tasks
Start with queries that represent different ways someone explores the market. For a search data category, that might look like this:
QUERIES = [
"serp api",
"google search api",
"serp data for seo tools",
"search api for ai agents",
"competitor rank tracking api",
]
The query set matters more than the size of the script. If every query has the same intent, the map will exaggerate one part of the market. Include category discovery, use-case, comparison, and implementation queries when they are relevant.
2. Collect structured organic results
The function below sends each query to TalorData SERP API and keeps the fields needed for analysis.
from urllib.parse import urlparse
import requests
ENDPOINT = "https://serpapi.talordata.net/serp/v1/request"
def normalize_domain(url):
host = urlparse(url).netloc.lower()
return host.removeprefix("www.")
def fetch_results(query):
response = requests.post(
ENDPOINT,
headers={
"Authorization": "Bearer <TALORDATA_TOKEN>",
"Content-Type": "application/x-www-form-urlencoded",
},
data={
"engine": "google",
"q": query,
"gl": "us",
"hl": "en",
"device": "desktop",
"json": "2",
},
timeout=60,
)
response.raise_for_status()
rows = []
for result in response.json().get("organic", []):
url = result.get("link") or result.get("url") or ""
if not url:
continue
rows.append({
"query": query,
"position": result.get("position"),
"domain": normalize_domain(url),
"title": result.get("title", ""),
"url": url,
})
return rows
Keep the country, language, device, and collection date consistent. Otherwise, changes in search context can look like differences in market structure.
3. Classify domains by market role
Domain classification needs judgment. A transparent lookup table is a good starting point because a reviewer can correct it.
CATEGORY_BY_DOMAIN = {
"example-vendor.com": "direct_vendor",
"example-directory.com": "directory",
"example-media.com": "publisher",
"example-community.com": "community",
}
def classify_domain(domain):
return CATEGORY_BY_DOMAIN.get(domain, "unclassified")
Do not silently force every unknown domain into a convenient category. Keep unclassified visible, review the most frequent unknowns, and update the lookup table. That makes the map auditable instead of pretending the categories are objective facts.
4. Aggregate visibility across queries
A simple position-weighted score gives higher-ranked results more influence while still rewarding repeated appearances.
from collections import defaultdict
def position_weight(position):
if not isinstance(position, int) or position < 1:
return 0
return 1 / position
all_rows = []
for query in QUERIES:
all_rows.extend(fetch_results(query))
domain_stats = defaultdict(lambda: {
"appearances": 0,
"queries": set(),
"visibility_score": 0.0,
})
for row in all_rows:
stats = domain_stats[row["domain"]]
stats["appearances"] += 1
stats["queries"].add(row["query"])
stats["visibility_score"] += position_weight(row["position"])
market_map = []
for domain, stats in domain_stats.items():
market_map.append({
"domain": domain,
"category": classify_domain(domain),
"appearances": stats["appearances"],
"query_coverage": len(stats["queries"]),
"visibility_score": round(stats["visibility_score"], 3),
})
market_map.sort(
key=lambda row: (row["query_coverage"], row["visibility_score"]),
reverse=True,
)
This is not a universal visibility metric. It is a practical comparison score for one controlled query set. Keep the formula documented so future snapshots remain comparable.
5. Produce two review tables
The domain table shows recurring sites:
domain,category,appearances,query_coverage,visibility_score
example-vendor.com,direct_vendor,4,3,1.583
example-media.com,publisher,3,3,0.867
example-directory.com,directory,2,2,0.750
Then aggregate the same rows by category:
category_stats = defaultdict(lambda: {
"domains": set(),
"appearances": 0,
"visibility_score": 0.0,
})
for row in market_map:
stats = category_stats[row["category"]]
stats["domains"].add(row["domain"])
stats["appearances"] += row["appearances"]
stats["visibility_score"] += row["visibility_score"]
category_map = [
{
"category": category,
"domain_count": len(stats["domains"]),
"appearances": stats["appearances"],
"visibility_score": round(stats["visibility_score"], 3),
}
for category, stats in category_stats.items()
]
The category table reveals whether discovery is controlled mainly by vendors or by intermediaries such as publishers and directories. That distinction can change your content and partnership strategy.
6. Add the limits to the report
A SERP market map is a search snapshot, not a complete company database. Record the query set, collection date, location, language, device, category definitions, and scoring formula beside the output. Also keep the original URLs so someone can inspect why a domain was included.
For a first pass, TalorData SERP API provides structured Google results that can feed this workflow. New accounts include 500 responses, which can support a focused multi-query map before you expand the query set.
The main shift is simple: stop reading the SERP as ten isolated rankings. Aggregate related searches, classify the domains by role, and turn repeated visibility into a market structure you can review.
Top comments (0)