DEV Community

Elowen
Elowen

Posted on

Create a Location-Aware SERP Monitor for Local SEO Campaigns

A local SEO rank check becomes more useful when it keeps the location context attached to every result. Without that context, a report can say that a page ranks position 4 for a keyword, but it cannot explain where that position was observed, which market changed, or whether two locations are showing different competitors.

This post walks through a small location-aware SERP monitor that stores keyword, location, language, country, rank, URL, and snapshot time in one table. The goal is not to build a full SEO platform. The goal is to create a repeatable output that a local SEO team can inspect every week.

What the monitor should answer

For each keyword, the monitor should answer a few practical questions:

  • What does Google return for this keyword in each target location?
  • Which URL from our domain appears, if any?
  • What position was observed?
  • Which competitors are visible in the same location?
  • Did the result change compared with the previous snapshot?

The important part is that the location is not a note in a file name. It is a first-class field in the output.

Minimal table design

A useful local rank table can start with these columns:

snapshot_date
keyword
location_label
gl
hl
device
rank
result_title
result_url
result_domain
matched_own_domain
is_own_result
serp_top_domains
notes
Enter fullscreen mode Exit fullscreen mode

This structure keeps the ranking evidence and the local context together. If the same keyword is checked for New York, Austin, and Toronto, those become three separate rows instead of one vague metric.

Requesting a location-aware SERP

TalorData SERP API accepts Google SERP parameters such as q, location, gl, hl, device, and json=2. A simple request can look like this:

curl -X POST 'https://serpapi.talordata.net/serp/v1/request' \
  -H 'Authorization: Bearer <TALORDATA_TOKEN>' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'engine=google' \
  -d 'q=emergency plumber near me' \
  -d 'location=Austin, Texas, United States' \
  -d 'gl=us' \
  -d 'hl=en' \
  -d 'device=desktop' \
  -d 'json=2'
Enter fullscreen mode Exit fullscreen mode

If you want to build this into a script, keep the keyword and location list outside the request function. That makes it easier to add new cities without touching the API logic.

Example keyword and location config

KEYWORDS = [
    "emergency plumber near me",
    "water heater repair",
    "drain cleaning service",
]

LOCATIONS = [
    {
        "label": "Austin, TX",
        "location": "Austin, Texas, United States",
        "gl": "us",
        "hl": "en",
    },
    {
        "label": "Dallas, TX",
        "location": "Dallas, Texas, United States",
        "gl": "us",
        "hl": "en",
    },
    {
        "label": "Toronto, ON",
        "location": "Toronto, Ontario, Canada",
        "gl": "ca",
        "hl": "en",
    },
]
Enter fullscreen mode Exit fullscreen mode

This format also makes the final report more readable. The report can show Austin, TX while the request still sends the full location value.

Normalize organic results

The SERP response can include an organic array. For monitoring, normalize it into flat rows before doing any comparison.

from urllib.parse import urlparse

OWN_DOMAIN = "example.com"


def domain_from_url(url: str) -> str:
    host = urlparse(url).netloc.lower()
    return host[4:] if host.startswith("www.") else host


def normalize_organic(snapshot_date, keyword, location_cfg, response_json):
    rows = []

    for item in response_json.get("organic", []):
        url = item.get("link") or item.get("url") or ""
        domain = domain_from_url(url) if url else ""
        position = item.get("position") or item.get("rank")

        rows.append({
            "snapshot_date": snapshot_date,
            "keyword": keyword,
            "location_label": location_cfg["label"],
            "gl": location_cfg["gl"],
            "hl": location_cfg["hl"],
            "rank": position,
            "result_title": item.get("title", ""),
            "result_url": url,
            "result_domain": domain,
            "matched_own_domain": OWN_DOMAIN,
            "is_own_result": domain == OWN_DOMAIN or domain.endswith("." + OWN_DOMAIN),
        })

    return rows
Enter fullscreen mode Exit fullscreen mode

Keep this normalization boring. The monitor is easier to debug when every row represents one observed organic result in one location.

Reduce rows into a weekly monitor

Once you have normalized rows, you can produce a smaller monitoring table for stakeholders. For example, you may only need the first own-domain result and the top visible competitor domains.

def summarize_location(keyword, location_label, rows):
    location_rows = [
        row for row in rows
        if row["keyword"] == keyword and row["location_label"] == location_label
    ]

    own_rows = [row for row in location_rows if row["is_own_result"]]
    own_rows = sorted(own_rows, key=lambda row: row["rank"] or 999)

    top_domains = []
    for row in sorted(location_rows, key=lambda row: row["rank"] or 999)[:10]:
        domain = row["result_domain"]
        if domain and domain not in top_domains:
            top_domains.append(domain)

    first_own = own_rows[0] if own_rows else None

    return {
        "keyword": keyword,
        "location_label": location_label,
        "own_rank": first_own["rank"] if first_own else None,
        "own_url": first_own["result_url"] if first_own else "",
        "top_domains": ", ".join(top_domains[:5]),
        "note": "Own result not found in observed organic results" if not first_own else "",
    }
Enter fullscreen mode Exit fullscreen mode

This gives you a compact output without losing the evidence. If a ranking drops in one city, you can go back to the normalized rows and inspect what changed.

Add change detection

A local SEO monitor becomes more useful when it compares the current snapshot with the previous one.

def compare_rank(previous_rank, current_rank):
    if previous_rank is None and current_rank is None:
        return "not_visible"
    if previous_rank is None:
        return "newly_visible"
    if current_rank is None:
        return "lost_visibility"
    if current_rank < previous_rank:
        return "improved"
    if current_rank > previous_rank:
        return "declined"
    return "unchanged"
Enter fullscreen mode Exit fullscreen mode

The final report should not only say rank = 7. It should say something like declined from 3 to 7 in Austin, TX, or newly visible in Toronto, ON.

What to watch for

Location-aware monitoring has a few practical traps:

  • Do not mix desktop and mobile results in the same comparison.
  • Do not compare different countries without keeping gl visible.
  • Do not hide the observed location behind a generic market label.
  • Do not treat one city result as a national ranking.
  • Do not overwrite old snapshots; append new ones.

For local SEO, the audit trail matters. The team needs to know when a snapshot was taken, what parameters were used, and what result was actually observed.

Final output

A simple weekly output can look like this:

Date: 2026-08-07
Keyword: emergency plumber near me
Location: Austin, TX
Device: desktop
Own rank: 5
Own URL: https://example.com/austin/emergency-plumbing
Top visible domains: yelp.com, angi.com, example.com, localcompetitor.com
Change: declined from 3 to 5
Note: review local landing page title and competing directory pages
Enter fullscreen mode Exit fullscreen mode

That is much more useful than a single average position. It tells the team where the observation came from and what to inspect next.

If you are building this kind of monitor, TalorData SERP API can provide structured Google SERP data for location-aware checks. New accounts can start with 500 responses, which is enough to test a small keyword-location matrix before expanding the monitor.

Top comments (0)