cloro
Research

Local Rank Tracking: How Google Rankings Change by City, ZIP Code, and Map Pack

Ricardo Batista
Founder, cloro
10 min read
Rank TrackingLocal SEOSERP
On this page

Ask most rank trackers “where do I rank for emergency plumber?” and you get a single number. That number is a fiction. Google does not have one ranking for a local-intent keyword — it has a different ranking for every place a person could be standing when they search.

This is not a rounding error. Google’s own Business Profile guidance is explicit: local results are “mainly based on relevance, distance, and popularity,” where distance is “how far each business is from the customer who’s searching.” Proximity is baked into the algorithm. So the honest unit of local rank tracking is not the keyword — it is the keyword times the city (and often the keyword times the ZIP code).

Illustrative example: the query “emergency plumber” ranks #2 in Austin, #3 in Denver, #7 in Brooklyn, #12 in Phoenix, and does not rank in Portland — one keyword, five very different Google positions by city. Not study data.ILLUSTRATIVE — NOT STUDY DATAQUERY“emergency plumber”The same query, one week, five US cities →Austin, TX#2Denver, CO#3Brooklyn, NY#7Phoenix, AZ#12Portland, ORnot rankingGoogle recomputes local results per location by design.A national rank reports one number — an average nobody actually sees.

Illustrative schematic — example positions, not the study dataset. The measured 100-keyword × 20-city heatmap is in the data study below.

This post does two things. First, it measures how much rankings actually move across geography — a data study of the same keyword set scraped across a grid of US cities, plus how often the local map pack reshuffles. Second, it shows you how to check it yourself: a runnable google maps rank checker and a geo-parameterized tracking loop you can point at any city grid.

If what you need is the packaged product rather than the build, our local rank tracking page documents the city-level API directly. This post is the study and the how-to underneath it.

How much do Google rankings actually change by city?

To move this from anecdote to measurement, the study design is deliberately boring and repeatable:

  • Keyword set: 100 local-intent keywords (near-me terms, service + city patterns, and category head terms).
  • Geography: 20 US cities spanning regions, metro sizes, and time zones.
  • Method: one geo-parameterized SERP call per keyword × city, rendering the results that a device in that city would receive — not a national default.
  • Metric: for each keyword, a local variance score normalized to 0–1 across the 20 cities. A score of 0 means the ranking set is identical everywhere; a score near 1 means the keyword is maximally city-specific (a different winner in nearly every city).

The output is a keyword × city position heatmap: rows are keywords, columns are cities, and each cell is the target’s position in that city. Bands of consistent color mean a national tool would be roughly right; scattered color means a national number is actively misleading.

Geo-variance heatmap: 100 keywords by 20 US citiesEach cell shows how much a city’s top-5 organic results differ from the national consensus. Keywords are sorted by geo-variance; local-intent terms cluster at the top (dark, scattered), generic terms at the bottom (light, uniform).100 keywords · sorted by geo-varianceNYCLAXCHIHOUPHXPHLSATSANDALSJCATLMIASEADENBOSMSPCLTPDXBNASLCemergency dentist 0.96personal injury lawyer 0.86best internet providers 0.59best robot vacuum 0.14best hybrid suv 0.02how much a city differs from the national consensus (top-5)0 · same as most cities1 · entirely differentlocal intentgeneric

The same 100 keywords scraped across 20 US cities in one day. Each cell is how far that city’s top result set sits from the national consensus (the domains most cities share); keywords are sorted by geo-variance. Local-intent terms (orange) pile up at the dark, scattered top; generic terms fade to a uniform national result at the bottom.

Average local-variance score (100 keywords × 20 cities, steady across three days): 0.53 on a 0–1 scale — but that average hides the real story. Local-intent keywords average 0.84; generic keywords average 0.22. Near-me and “service + near me” terms cluster at the high-variance end, broad informational terms at the low end.

What three days of scraping show

Run the full grid — 100 keywords across 20 cities, 2,000 geo-targeted SERPs a day, for three days — and the score settles the question it exists to answer. The result is not a smooth gradient; it is a clean split by intent:

  • Local-intent keywords averaged 0.84, and 47 of the 50 scored above 0.7. For emergency dentist, pediatrician near me, and veterinarian near me, 17 to 19 of the 20 cities returned a different #1 result — a national rank for these is close to meaningless.
  • Generic keywords averaged 0.22, and 41 of the 50 scored below 0.3. best email marketing software and how to improve credit score returned the same #1 in every city.
  • No crossover at the extremes: not one local keyword fell in the low band, and not one generic keyword reached the high band. Intent alone sorts “must track per city” from “safe to track nationally.”

You can see the whole pattern in a single pair. plumber near me returns a different set of local businesses in nearly every city — New York: Rite Plumbing NYC, Roto-Rooter’s New York page, Petri Plumbing (Brooklyn); Philadelphia: Philly Plumbing Pros, Goodman Plumbing, Mount Airy Plumbing; Phoenix: Parker & Sons, The Plumber Guy, AZ Family Plumbing. Run best crm software against the same cities and you get the same national pages in the same order — Reddit, Salesforce, G2, Forbes — everywhere. The two segments are even won by different kinds of site: local terms by directories and booking platforms (Yelp, Thumbtack, Zocdoc), generic terms by national editorial and review sites (Reddit, Consumer Reports, Forbes).

The split held at these levels on all three days — 0.84 local and 0.22 generic each day — so it is a stable pattern, not a one-day artifact. The map-pack dimension is below.

The mechanism behind the numbers is the part you can rely on today. Because Google weights distance as a primary factor, terms with strong local intent (“best dentist near me”, “emergency plumber”) are the most geographically volatile — the winner in one metro is often invisible two metros away. Broad, informational queries (“what is a root canal”) move far less, because proximity barely factors into them. The variance score is just a way to rank your own keyword set from “safe to track nationally” to “must be tracked per city.”

The practical takeaway does not need the final figure to land: if a keyword has local intent, a national rank is the wrong instrument. It reports an average nobody actually experiences.

Why proximity turns local rankings into a moving target

It is worth being precise about why this happens, because it shapes what you should measure. Google names three factors for local results, and only one of them is about your page:

  • Relevance — how well your Business Profile and content match the query.
  • Distance — how far your business is from the searcher. This is the geographic term, and it is out of your control: it changes with the searcher, not with you.
  • Prominence — how well-known the business is, which Google says is influenced by “how many websites link to your business and how many reviews you have.”

Two of the three factors move independently of your website. Distance moves with the user; prominence moves with your reviews and citations over time. That is why local rankings behave like a moving target and why Google states plainly that “there’s no way to request or pay for a better local ranking.” You are being ranked on a surface that is re-computed for every location — which is exactly why per-city measurement, not a national average, is the only honest way to track it.

Prominence is also the reason review velocity matters for local visibility. In BrightLocal’s Local Consumer Review Survey 2026 — a panel of 1,002 US consumers — 97% of consumers read reviews for local businesses, and use of generative AI tools like ChatGPT for local recommendations jumped from 6% to 45% in a year, making it the third most popular source of business recommendations. Reviews feed the prominence signal Google names, and they increasingly feed the AI answer layer that sits alongside the map pack.

How often does the local map pack change? 3-pack volatility

Organic position is only half of local search. Above the ten blue links, Google shows the local pack — the “3-pack” of map listings — for most local-intent queries. It is a separate ranking system with its own logic, and it is noticeably more volatile than organic, because proximity and review signals shift underneath it constantly.

The second half of the study measures that churn directly:

  • Method: capture the three local-pack members for each keyword × city on every daily run.
  • Metric: day-to-day membership churn — the share of the three slots whose occupant changed from one day to the next. A churn of 0 means the same three businesses held their slots; a churn of 1 means the entire 3-pack turned over.

This is the number that surprises multi-location brands: a business can hold a stable organic position while its map-pack slot appears and disappears from day to day, because the pack is re-ranked against a moving proximity-and-reviews baseline.

Median day-to-day 3-pack churn across the city grid: ~16% — across three days, roughly one of the three slots changes hands from one day to the next, ranging from about 9% in the calmest metros to 23% in the most competitive. Over the same days the organic #1 moved only ~8%, so the pack reshuffles about twice as often as organic.

The structural point behind the number is independently supported: because the pack is driven by distance and prominence — both of which move — a daily snapshot is the minimum honest cadence. If you track the map pack, track it at least daily, and track it per city. For the broader pattern of how positions move over time across our monitoring corpus, see our rank volatility research.

The pack itself: as local as it gets

Beyond how fast it moves, capturing the pack itself — the same 100 keywords across the 20 cities — shows how intensely local the module is, and it held the same shape on all three days. The 3-pack appeared for 100% of local-intent queries, and its top slot was a different business in all 20 cities for plumber near me, emergency dentist, coffee shop near me, and personal injury lawyer (gym near me: 19 of 20). Roughly 2,750 distinct businesses held a pack slot each day. Search plumber near me and the winner is A&E NYC Plumbing in New York, Walsh & Son in Chicago, I Am Your Plumber in Houston, and Near Me Plumbing & Drains in Phoenix — no overlap between any of them. Generic queries almost never surface a pack (~5%), which is exactly why the map pack is a local tracking problem, not a national one.

This measures how city-specific the pack is — the complement to the churn figure above, which measures how fast it moves.

Google Maps rank checker: check rankings programmatically

Here is the part that captures the “checker” intent honestly — not a fabricated leaderboard, but code you can run. A google maps rank checker is, mechanically, a loop that asks Google the same question from many locations and records where your business lands in each one.

The only thing that changes between a national rank check and a city-level one is a single location value on the request. cloro’s /v1/monitor/google endpoint renders the SERP the way a device in that city sees it and returns both organicResults (each with a position and link) and — for local-intent queries — a localResults array, the local 3-pack members themselves. So one call checks both surfaces:

import requests

CLORO_API = "https://api.cloro.dev/v1/monitor/google"
API_KEY = "sk_live_your_api_key_here"

# The business you want to check.
TARGET = "mybusiness.com"      # matched on domain/listing URL in the organic list
TARGET_NAME = "My Business"    # matched on business name in the local 3-pack
QUERY = "plumber near me"

# A small city grid. Use Google's canonical "City,Region,Country" names.
CITY_GRID = [
    "Austin,Texas,United States",
    "Round Rock,Texas,United States",
    "Cedar Park,Texas,United States",
    "Pflugerville,Texas,United States",
]

def check_city(query, location):
    try:
        resp = requests.post(
            CLORO_API,
            headers={
                "Authorization": f"Bearer {API_KEY}",
                "Content-Type": "application/json",
            },
            json={
                "query": query,
                "country": "US",
                "location": location,   # the only field that changes per city
                "device": "desktop",    # cloro returns the local pack on the desktop response
            },
            timeout=30,
        )
        resp.raise_for_status()
    except requests.RequestException as exc:
        # A rate limit or timeout on one city shouldn't abort the whole grid.
        print(f"  request failed for {location}: {exc}")
        return None, None
    # .get() chaining so an off-shape response returns {} instead of raising.
    result = resp.json().get("result", {})
    # Organic position — matched on the target's domain/URL.
    organic = next((r["position"] for r in result.get("organicResults", [])
                    if TARGET in r.get("link", "")), None)
    # Local 3-pack slot — cloro returns the pack as `localResults`; match on name.
    pack = next((p["position"] for p in result.get("localResults", [])
                 if TARGET_NAME.lower() in (p.get("title") or "").lower()), None)
    return organic, pack

for city in CITY_GRID:
    organic, pack = check_city(QUERY, city)
    o = f"#{organic}" if organic else "—"
    p = f"pack #{pack}" if pack else "not in pack"
    print(f"{QUERY:<18} | {city:<32} | organic {o:<5} | {p}")

This is genuinely runnable: real endpoint, real parameters, real response fields. The positions it prints are whatever the live SERP returns for each city when you run it — this post deliberately shows the method and the output format, not invented rankings. Swap in your API key, your target, and your own city grid, and you have a working checker.

A few honest notes on what you are and are not measuring:

  • Organic and the map pack, from one call. The loop reads both lists cloro returns: result.organicResults and result.localResults — the local 3-pack members, each with a position, title, rating, and reviews. They are distinct ranking systems, so a business can rank organically and still miss the pack, or vice versa; reading both is the only way to see the full local picture. See the local rank tracking API reference for the fields available per query.
  • Device matters. Local-intent queries skew heavily to mobile, and the mobile SERP orders modules differently (the pack often sits at the very top). cloro surfaces the localResults pack on the desktop response, which is why this checker sends device: "desktop" — set it deliberately for what you need.
  • “Not ranking” is data. A None result for a city is a real finding, not a failure — it means the target did not appear in the results that city sees, which is exactly the blind spot a national tool hides.

Building a city- and ZIP-level rank tracker

A one-off checker is useful; a tracker that runs on a schedule and diffs itself over time is what actually catches local movement. The upgrade path from the snippet above is small and reuses machinery we have already documented, so this section builds on top rather than repeating it.

1. Expand the grid to keyword × city (× ZIP where it matters). The checker loops cities for one keyword; a tracker loops the full cross-product. One API call per (query, location, device) combination is the whole model — a 50-city, 20-keyword daily run is just 1,000 calls a day, at the same per-call price as national tracking.

2. Store one row per (keyword, city, ranked URL) and diff runs over time. The schema, the nightly scheduler, and the position-diff query are identical to national rank tracking — only the location dimension is added. Rather than restate it, follow the storage-and-diff pattern in our Google rank tracking API guide and add location to the primary key. For the general workflow of scheduling and reading SERP positions over time, track SERP rankings covers the moving parts.

3. Go tighter than a city with uule. City names are enough for most tracking. When rankings differ within a city — common for dense metros and pure near-me terms — you need point-level precision, which is what Google’s uule parameter provides: a base64-encoded location that spoofs GPS coordinates. You do not have to hand-roll it. cloro’s location value already resolves to the correct uule plus gl and hl; if you would rather build the request yourself, the Google search parameters guide documents gl, hl, and the uule encoding in full. The tracker’s shape does not change — only how you obtain each per-point SERP does.

The design goal is one honest sentence: track the keyword times the location, at the granularity where the ranking actually changes. For most local terms that is the city; for near-me and dense-metro terms it is the ZIP or the point.

What this means for local SEO

Three takeaways survive regardless of where the final study figures land:

  1. A national rank is the wrong instrument for a local keyword. Google recomputes local results per location by design, so an average position describes a searcher who does not exist. Score your keyword set by local variance and track the volatile ones per city.
  2. The map pack needs its own, more frequent tracking. It is a separate ranking system driven by distance and reviews — both moving targets — so a weekly check understates its churn. Daily, per-city capture is the honest cadence.
  3. Prominence is a compounding local asset. With 97% of consumers reading local reviews and AI recommendation tools rising fast, the review signal that feeds Google’s prominence factor is the same signal feeding the AI answer layer. Reviews are no longer just reputation — they are ranking input on two surfaces at once.

The through-line is measurement. You cannot manage a ranking you are averaging away. Track it where it actually lives — per city, per pack, per point — and the “why is my ranking bouncing?” mystery usually resolves into “you were watching a national number for a local query.” When you are ready to run this continuously across your locations, cloro’s local rank tracking is the same call shown above, on a schedule.

Ricardo Batista

About the author

Founder, cloro

Ricardo is one of the founders and engineers behind its SERP and AI-search scraping infrastructure. Before cloro he scaled a financial comparison site to $7M ARR and ran the full-country operations of a unicorn to $65M ARR, then went back to building. He writes about search engine scraping, generative-engine optimization, and turning live search and AI-answer data into something teams can act on.

Frequently asked questions

What is a Google Maps rank checker?+

A Google Maps rank checker is a tool or script that queries Google from a specific location and reports where a business appears in the results that city actually sees — its organic position and, where present, its slot in the local map pack. Because Google localizes results by proximity, a checker has to pin the request to a location (a city, or a precise point via the uule parameter) rather than search from a default IP.

Why do my Google rankings change from city to city?+

Google's own guidance states that local results are mainly based on relevance, distance, and prominence. Distance means how far each business is from the searcher, so the same query returns a different ranking in Austin than in Brooklyn. A single national position is an average that hides this per-city reality.

What is the Google local pack (3-pack)?+

The local pack — often called the 3-pack — is the module of map-based business listings Google shows above the organic results for local-intent queries. It is a separate ranking system from organic, driven heavily by proximity, prominence, and reviews, and its membership can change week to week.

Can I track local rankings by ZIP code?+

Yes. City-level tracking uses Google's canonical location names, and for tighter granularity you can encode a precise point with the uule parameter, which spoofs GPS coordinates. Rankings can differ between ZIP codes within the same city, so ZIP- or point-level tracking is the honest granularity for multi-location and near-me terms.

Do I need the uule parameter to track city-level rankings?+

Not directly, if your SERP API accepts a location name and handles the encoding for you. Under the hood, city-level results come from Google's gl, hl, and uule parameters. cloro's location value resolves to the right uule plus gl and hl, so you pass a city name instead of doing the base64 math yourself.