<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: GBP Local Ranker</title>
    <description>The latest articles on DEV Community by GBP Local Ranker (@gbp_localranker).</description>
    <link>https://dev.to/gbp_localranker</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4166769%2F739e9a41-079f-4211-9a7f-da4766fc2a05.png</url>
      <title>DEV Community: GBP Local Ranker</title>
      <link>https://dev.to/gbp_localranker</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gbp_localranker"/>
    <language>en</language>
    <item>
      <title>Building a Geo-Grid Rank Check: How Local Rank Tracking Works Under the Hood</title>
      <dc:creator>GBP Local Ranker</dc:creator>
      <pubDate>Thu, 08 Oct 2026 13:31:05 +0000</pubDate>
      <link>https://dev.to/gbp_localranker/building-a-geo-grid-rank-check-how-local-rank-tracking-works-under-the-hood-3ca3</link>
      <guid>https://dev.to/gbp_localranker/building-a-geo-grid-rank-check-how-local-rank-tracking-works-under-the-hood-3ca3</guid>
      <description>&lt;p&gt;If you have ever built a tool for a local business client, you have probably been asked a deceptively simple question: "Where do we rank on Google Maps?" The honest answer is "it depends where you are standing". Local results are personalised by location, so a single rank number hides most of the picture.&lt;/p&gt;

&lt;p&gt;Geo-grid rank tracking is the technique most local SEO tools use to answer the question properly. This post walks through how it works conceptually, the maths behind building the grid, and the design decisions that matter if you build one yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea
&lt;/h2&gt;

&lt;p&gt;Instead of checking a keyword once, you check it from many coordinates arranged in a grid around the business. Each point records the position of the target business in the local results for that keyword from that location. Plot the results on a map, colour the points by rank, and you get a heat map of visibility.&lt;/p&gt;

&lt;p&gt;The output answers questions a single rank cannot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How far does visibility extend from the business?&lt;/li&gt;
&lt;li&gt;Which neighbourhoods does a competitor dominate?&lt;/li&gt;
&lt;li&gt;Did a profile change improve coverage, or just the rank at the front door?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Generating the grid
&lt;/h2&gt;

&lt;p&gt;A grid is defined by a centre point (usually the business's coordinates), a size (for example 7Ã—7 points) and a spacing (for example 1 km between points). Because the Earth is not flat, converting kilometres into latitude and longitude offsets needs a little care. For the small distances involved, a simple approximation is fine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_grid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spacing_km&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return a size x size list of (lat, lng) points centred on lat/lng.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;half&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="n"&gt;km_per_deg_lat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;111.32&lt;/span&gt;
    &lt;span class="n"&gt;km_per_deg_lng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;111.32&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;radians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;points&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;half&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;half&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;half&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;half&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;d_lat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;spacing_km&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;km_per_deg_lat&lt;/span&gt;
            &lt;span class="n"&gt;d_lng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;spacing_km&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;km_per_deg_lng&lt;/span&gt;
            &lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;d_lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lng&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;d_lng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;points&lt;/span&gt;

&lt;span class="n"&gt;grid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_grid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;40.7128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;74.0060&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spacing_km&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# 25 points
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the cosine term: a degree of longitude shrinks as you move away from the equator, so the longitude step must grow to keep the spacing even.&lt;/p&gt;

&lt;h2&gt;
  
  
  Collecting results per point
&lt;/h2&gt;

&lt;p&gt;For each point you need the local results as a searcher at that location would see them. How you obtain them matters. Scraping Google directly breaks its terms of service and is fragile, so production tools generally rely on licensed data providers or official APIs where they fit the use case. Whatever the source, store the raw result list for each point, not just the target's position. Competitor data is often the most valuable part.&lt;/p&gt;

&lt;p&gt;A minimal record might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keyword"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"emergency plumber"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"point"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;40.7128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;-74.0060&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scanned_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-08T09:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target_rank"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"top_results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"place_id_a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"place_id_b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"place_id_c"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Design decisions that matter
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Grid size versus cost.&lt;/strong&gt; A 9Ã—9 grid is 81 lookups per keyword per scan. Multiply by keywords, locations and scan frequency and costs climb quickly. Most teams settle on a few priority keywords per location.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spacing should match the business.&lt;/strong&gt; A city-centre cafÃ© might use 300 to 500 metre spacing; a rural roofing company might need several kilometres. If every point shows the same rank, your spacing is probably too small to be useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consistency over time.&lt;/strong&gt; Keep the centre, size, spacing and keywords identical between scans. Otherwise you are comparing different experiments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Handle "not found".&lt;/strong&gt; Most providers return a limited number of results. Record positions beyond that as "not in top N" rather than guessing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Summarise carefully.&lt;/strong&gt; A common summary is the average rank across all points, but averages hide shape. Pair it with the share of points where the business appears in the top three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Presenting the results
&lt;/h2&gt;

&lt;p&gt;Clients understand colour faster than tables. Green for top three, amber for four to ten, red for beyond. Show scans side by side over time, and annotate changes such as "primary category updated" so the history tells a story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build or buy?
&lt;/h2&gt;

&lt;p&gt;Building a basic grid tracker is a fun weekend project. Running one reliably for many clients, with data costs, scheduling, history and reports, is a bigger commitment. Hosted platforms such as &lt;a href="https://gbplocalranker.com" rel="noopener noreferrer"&gt;GBP Local Ranker&lt;/a&gt; package grid scans with profile audits and white-label reports, which may make more sense if local SEO is not your core product.&lt;/p&gt;

&lt;p&gt;If you are a developer who simply needs a client's Business Profile sorted before handing over a new website, you can also &lt;a href="https://www.fiverr.com/gbplocalranker" rel="noopener noreferrer"&gt;hire a GBP expert on Fiverr&lt;/a&gt; to handle the profile side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Geo-grid tracking turns "where do we rank?" into a question you can answer honestly: here is where you are visible, here is where you are not, and here is how that changes over time. The maths is simple; the value comes from consistent measurement and careful interpretation.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I work with the GBP Local Ranker team.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>googlemaps</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
