# Google Maps Rank Tracker: Rank Grid API for Local SEO (`ocius-data/google-maps-rank-grid`) Actor

Geo-grid local rank tracker for Google Maps: rank at every grid point, top 3 competitors and a heatmap. A Local Falcon alternative, MCP-ready.

- **URL**: https://apify.com/ocius-data/google-maps-rank-grid.md
- **Developed by:** [Ocius Data](https://apify.com/ocius-data) (community)
- **Categories:** SEO tools, Marketing, Agents
- **Security:** Limited permissions
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.20 / 1,000 grid point checkeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Google Maps Rank Grid Tracker (Local SEO)

**What the three most-used geo-grid trackers in the Store do not give you:** at every point, the business ranked just above you (`competitorAbove`), its rating and reviews gap, and a plain-language "why this rank" (`rankExplanation`). Every run also includes the rank change since your last run and a colour HTML heatmap, at no extra cost.

**Google Maps Rank Grid Tracker** checks where a business ranks on **Google Maps** at every point of a **geo-grid** around a centre, the way Local Falcon, BrightLocal or Local Viking do. Give a business (name, place ID or Maps URL), one or more keywords and a grid (3x3 to 9x9, spacing in km or miles). For each point you get the **rank of the business (1 to 20)**, the **top 3 competitors** with rating and reviews count, and, if you want, all 20 businesses Google shows there. Each keyword also gets a **free summary row** with average rank, **share of local voice** (points in the top 3), best and worst point and a **text heatmap**. You pay only per grid point checked successfully.

### Why this geo-grid rank tracker

- **Who to overtake, point by point.** `competitorAbove` names the business ranked just above yours at each point, with its rating and reviews gap, and `rankExplanation` says it in one sentence ("Rank 4. To enter the top 3 (quick win), overtake ..."). Points at rank 4 to 6 are flagged as **quick wins**. (Checked on 3 November 2026: the three most-used geo-grid trackers compare you with the overall leader or list the top results, but none names the business directly above you at each point.)
- **Rank change since the last run, built in.** Each point gets `previousRank`, `rankChange` (+ means you moved up) and `changeStatus`; the summary row gets the change of the average rank and of the share of local voice. The previous ranks are kept in a named key-value store in your account; there is nothing to set up.
- **HTML heatmap.** A colour heatmap for every keyword (record `HEATMAP`), linked from the summary row and the run's status message, ready to share with a client.
- **Real Google Maps results per point.** Each point is the same search Google Maps runs when a user looks for the keyword with the map centred there. No browser, no estimates.
- **Exact matching by place ID.** The Actor looks the business up first and matches it by place ID or CID at every point, so two branches of a chain or similar names are never confused.
- **Competitors at every point.** The top 3 businesses per point, and a ranking of the competitors that appear most often in the top 3 across the grid.
- **Ready-made metrics.** Average rank, average total rank (not found counted as 21), share of local voice, points in the top 3 and top 10, best and worst point, and a heatmap you can paste in a report.
- **Fair billing.** One `grid-point` event per point with Google results. Failed points, points with no businesses at all and the summary rows are free. Your maximum cost per run is respected.
- **Clear status per point.** Every point ends as `OK`, `NO_RESULTS`, `BLOCKED` or `ERROR` in the `RUN_SUMMARY` record. If more than 20% of points fail, the run is marked as failed so you notice.
- **Safe on long runs.** Progress is saved after every point. A platform restart never duplicates rows or charges twice.

### How to use it

1. Identify the business with one of:
   - **Business name**, as it appears on Google Maps, for example `Expert Dental - Midtown`. The Actor finds its place ID and location.
   - **Google Maps URL** of the listing (`https://www.google.com/maps/place/...` or `https://maps.app.goo.gl/...`). Best for one branch of a chain.
   - **Place ID** (`ChIJ...`). With a place ID only, also give the centre.
2. Add up to 10 **keywords**, as customers type them: `dentist`, `emergency plumber`, `pizza delivery`.
3. Choose the **grid**: size (3, 5, 7 or 9 points per side) and **spacing** between points in km or miles. By default the grid is centred on the business; you can give a centre latitude and longitude or a centre address instead.
4. Set **language** and **country** (for example `es` and `co` for Colombia).
5. Run the Actor. The status message already gives the result in one line, for example: `"dentist": avg rank 4.63 (up 0.4 since last run); top 3 in 2 of 9 points; 6 quick wins at rank 4-6; main competitor: ...`. Then open the **Overview**, **Grid points** and **Summary per keyword** views, the **HTML heatmap**, or download JSON, CSV or Excel.
6. Schedule the same input weekly: from the second run on, every point shows how the rank changed.

Example input: a 7x7 grid with 1 mile spacing for two keywords.

```json
{
  "businessName": "Expert Dental - Midtown",
  "keywords": ["dentist", "emergency dentist"],
  "gridSize": 7,
  "spacing": 1,
  "distanceUnit": "mi",
  "language": "en",
  "country": "us"
}
```

How big is the grid? Side length = (grid size - 1) x spacing. A 5x5 grid with 1 km spacing covers 4 x 4 km; a 9x9 grid with 2 km spacing covers 16 x 16 km.

### Output example and field dictionary

The dataset has two kinds of rows, told apart by `recordType` (same value as `rowType`): one row per point and keyword (`point`) and one free summary row per keyword (`summary`). The first view, **Overview**, shows both.

One row per point and keyword (`recordType: "point"`):

```json
{
  "recordType": "point",
  "rowType": "point",
  "status": "OK",
  "keyword": "dentist",
  "pointIndex": 13,
  "row": 3,
  "col": 3,
  "isCenter": true,
  "lat": 40.750371,
  "lng": -73.978035,
  "distanceFromCenterKm": 0,
  "rank": 1,
  "found": true,
  "inTop3": true,
  "isQuickWin": false,
  "previousRank": 2,
  "rankChange": 1,
  "changeStatus": "IMPROVED",
  "rankExplanation": "Rank 1: first local result here. Closest challenger: Diamond District Dental NYC - Midtown Dentist (4.9 stars, 611 reviews).",
  "competitorAbove": null,
  "resultsCount": 20,
  "businessName": "Expert Dental - Midtown",
  "businessPlaceId": "ChIJgSvKJARZwokRGGDLFY2oPEc",
  "top1Name": "Expert Dental - Midtown",
  "top2Name": "Diamond District Dental NYC - Midtown Dentist",
  "top3Name": "My NYC Dentist - 34th Street Dental",
  "top3": [
    {"rank": 1, "name": "Expert Dental - Midtown", "placeId": "ChIJgSvKJARZwokRGGDLFY2oPEc", "cid": "5133162999205027864", "rating": 4.5, "reviewsCount": 2314, "category": "Dentist"},
    {"rank": 2, "name": "Diamond District Dental NYC - Midtown Dentist", "placeId": "ChIJO8Bn7eRYwokRwZxgpuBFKlU", "cid": "6136794273412062401", "rating": 4.9, "reviewsCount": 611, "category": "Dentist"},
    {"rank": 3, "name": "My NYC Dentist - 34th Street Dental", "placeId": "ChIJex3pOaxZwokRqFhjqqYmnx0", "cid": "2134467245661968552", "rating": 4.7, "reviewsCount": 503, "category": "Dentist"}
  ],
  "matchedBy": "placeId/cid",
  "zoom": 14,
  "language": "en",
  "country": "us",
  "sourceUrl": "https://www.google.com/maps/search/dentist/@40.750371,-73.978035,14z?hl=en&gl=us",
  "checkedAt": "2026-11-02T02:43:23.405Z"
}
```

| Field | Meaning | When it is null |
|---|---|---|
| `recordType`, `rowType` | `point` for a grid point, `summary` for the summary of one keyword (both fields carry the same value). | Never |
| `status` | `OK`, `NO_RESULTS`, or, for a point that could not be checked, `BLOCKED`, `ERROR`, `TIMEOUT` or `NOT_PROCESSED` (free status row). | Never |
| `keyword` | Keyword searched. | Never |
| `pointIndex`, `row`, `col` | Point number (1 = north-west corner, row by row) and its position. Row 1 is the northernmost row, column 1 the westernmost. | Never |
| `isCenter`, `distanceFromCenterKm` | Whether this is the centre point, and its distance from the centre. | Never |
| `lat`, `lng` | Coordinates of the point. | Never |
| `rank` | Position of the business among the first 20 Google Maps results at this point. | When the business is not in the first 20 |
| `found`, `inTop3` | Whether the business appears in the first 20, and in the first 3. | On status rows of points that were not checked |
| `isQuickWin` | The business is at rank 4, 5 or 6: one to three places from the top 3. | On status rows |
| `previousRank` | Rank at the same point in the previous run with the same business, keyword and grid. | First run, point not in the top 20 last time, or comparison turned off |
| `rankChange` | Places gained since the previous run (+ = moved up, - = moved down). "Not in the top 20" counts as 21. | First run or point not comparable |
| `changeStatus` | `IMPROVED`, `DECLINED`, `UNCHANGED`, `NEW_IN_TOP20`, `LOST_FROM_TOP20`, `FIRST_RUN` or `NOT_COMPARABLE` (the point failed in the previous run). | On status rows |
| `rankExplanation` | One sentence on why the business has this rank here and who to overtake. | Never |
| `competitorAbove` | The business ranked directly above yours: rank, name, place ID, CID, rating, reviews count, category, `ratingGap` and `reviewsGap` (positive = it has more). | When your business is rank 1 or not in the top 20 |
| `businessRating`, `businessReviewsCount`, `businessCategory` | Rating, reviews count and main category of your business as Google shows them at this point. | When the business is not in the top 20 |
| `top1Rating`, `top1ReviewsCount` | Rating and reviews count of the first result. | When Google shows no business or no rating |
| `dataQuality`, `warnings` | `OK`, `CHECK` or `NOT_CHECKED`, and the reasons: `MATCHED_BY_NAME` (no place ID found, exact name used), `FEW_RESULTS` (fewer than 10 businesses here), `NO_BUSINESSES_AT_POINT`, `BUSINESS_RATING_MISSING`, or the status of a point that was not checked. | Never (empty list when nothing to flag) |
| `resultsCount` | Businesses Google returned at this point (up to 20). | Never |
| `businessName`, `businessPlaceId` | The tracked business. | `businessPlaceId` when only a name was given and the lookup found nothing |
| `top1Name`, `top2Name`, `top3Name` | Names of the first three businesses, flat for spreadsheets. | When Google shows fewer businesses |
| `top3` | First three businesses: rank, name, place ID, CID, rating, reviews count, main category, all categories, address, website and coordinates. | Empty list when no results; not present on status rows |
| `businesses` | Only with **Include all businesses per point**: all businesses at the point with rank, place ID, CID, rating, reviews count, category, address, coordinates and website. | Not present otherwise |
| `matchedBy` | `placeId/cid` (exact) or `name` (exact business name, used only when no identifier could be found). | Never |
| `zoom`, `language`, `country` | Search settings used. | Never |
| `sourceUrl` | The same search on Google Maps, to check it by hand. | Never |
| `input`, `checkedAt` | Input of the run and time of the check (ISO 8601, UTC). | Never |

One summary row per keyword (`recordType: "summary"`, **free**):

| Field | Meaning |
|---|---|
| `status` | `OK` (every point checked), `PARTIAL` (some points failed) or `FAILED` (no point checked). |
| `insight` | The result in one sentence with this run's numbers: average rank and its change, points in the top 3, points improved and declined, quick wins and main competitor. |
| `business` | Name, place ID, CID, rating, reviews count, category, address, coordinates and website of the tracked business. |
| `gridSize`, `spacingKm`, `centerLat`, `centerLng`, `centerSource` | Grid used and where its centre came from. |
| `pointsTotal`, `pointsChecked`, `pointsFailed` | Points in the grid, points checked and points that failed. |
| `pointsFound`, `pointsNotFound`, `pointsInTop3`, `pointsInTop10` | Points where the business is in the first 20, not in the first 20, in the top 3 and in the top 10. |
| `averageRank` | Average rank over the points where the business was found (null if found nowhere). |
| `averageTotalRank` | Average rank over all checked points, counting "not found" as 21. Lower is better; use it to compare runs. |
| `shareOfLocalVoice` | Percentage of checked points where the business is in the top 3. |
| `shareOfPointsInTop10`, `shareOfPointsFound` | Percentage of checked points in the top 10 and in the first 20. |
| `bestPoint`, `worstPoint` | Row, column, coordinates and rank of the best and worst point. |
| `topCompetitors` | The 5 businesses that appear most often in the top 3 across the grid, with their appearances, average rank in the top 3, rating and reviews count. |
| `quickWinPoints`, `quickWins` | Number and list of points at rank 4 to 6, with the business in third place there. |
| `previousRunAt` | Time of the previous run used for the comparison (ISO 8601, UTC). Null on the first run. |
| `previousAverageRank`, `averageRankChange` | Average rank of the previous run and the change (+ = better). |
| `previousAverageTotalRank`, `averageTotalRankChange` | The same for the average total rank. |
| `previousShareOfLocalVoice`, `shareOfLocalVoiceChange` | Share of local voice of the previous run and the change in percentage points. |
| `previousPointsInTop3`, `pointsImproved`, `pointsDeclined`, `pointsUnchanged` | Top-3 points last time, and how many points moved up, down or stayed. Null on the first run. |
| `heatmapUrl` | Link to the colour HTML heatmap of this run (record `HEATMAP`). |
| `dataQuality`, `warnings` | `OK` or `CHECK`, with `MATCHED_BY_NAME`, `SOME_POINTS_NOT_CHECKED` or `BUSINESS_NOT_FOUND_ANYWHERE`. |
| `heatmap`, `heatmapRows` | Text heatmap, north on top: the rank at each point, `--` when not in the first 20, `??` when the point failed or was not checked, the centre in brackets. |

Heatmap example (5x5, 1 km, the same run):

```
  1   1   1   1  --
  1   1   1   2  --
  1   1 [ 1]  1  --
  2   2   1  --  --
 --  --  --  --  --
```

#### Key-value store records

| Record | Content |
|---|---|
| `RUN_SUMMARY` | Resolved business and centre, status count per point, the summary and insight of every keyword, the comparison settings, proxy usage, warnings and the reconciliation between rows saved and events charged. |
| `HEATMAP` | Colour HTML heatmap, one grid per keyword, with the change since the previous run on every cell and the competitors most often in the top 3. Self-contained (no scripts). |
| `STATE` | Internal progress, so a platform restart never duplicates rows or charges. |
| `INPUT` | The input of the run. |

The named key-value store `maps-rank-grid-history` (field **History store name**) keeps one record per business, keyword and grid with the rank at every point of the last run. Delete it to start the comparison over. Turn **Compare with the previous run** off to skip it.

The output tab links every view, the grid points as CSV, the HTML heatmap and `RUN_SUMMARY`.

### Pricing and cost calculator

**Typical job cost:** a 5x5 grid for 1 keyword costs about USD 0.15 (25 points); a 5x5 grid for 3 keywords about USD 0.45 (75 points). Platform compute is included; the summary rows, the comparison with the previous run and the HTML heatmap are free.

**Apify Store discounts:** `grid-point` costs 15% less on Silver and 30% less on Gold and above.

Pay per event: USD 0.006 per `grid-point`, one per point and keyword checked successfully. The only other charge is Apify's standard start event (`apify-actor-start`), USD 0.00005 per run at the default memory.

| Event | When it is charged |
|---|---|
| `grid-point` | One grid point with Google Maps results, saved to the dataset (whether the business is in the first 20 or not). |

**Free plan:** the USD 5 of monthly usage included in the Apify Free plan covers about 830 grid points (USD 0.006 each): about 33 scans of a 5x5 grid or 17 scans of a 7x7 grid for one keyword.

Never charged: failed points, points where Google lists no business at all, the summary rows, invalid inputs and the business lookup. Your maximum cost per run is respected: the Actor stops before checking a point it cannot charge.

Points per run = grid size x grid size x keywords. Speed: a 5x5 grid for one keyword finished in 8 to 19 seconds in five test runs on 4 November 2026.

| Scan | Points | Cost |
|---|---|---|
| 3x3, 1 keyword | 9 | USD 0.054 |
| 5x5, 1 keyword | 25 | USD 0.15 |
| 7x7, 1 keyword | 49 | USD 0.29 |
| 9x9, 1 keyword | 81 | USD 0.49 |
| 7x7, 5 keywords | 245 | USD 1.47 |
| Weekly 7x7 for 3 keywords, 4 weeks | 588 per month | USD 3.53 per month |

Formula: `points × 0.006`. Prices shown are for the Free and Bronze plans; Silver and Gold pay less. **Defects:** if a run delivers wrong ranks because of a defect in the Actor, open an issue with the run ID. Defects are fixed with priority, usually within 72 hours.

### Integrations: schedules, API, webhooks, Sheets and AI agents

**Schedules.** Save your input as a task and schedule it weekly in **Schedules** to follow your local rankings over time. Each run compares itself with the previous one automatically (`rankChange` per point, `averageRankChange` and `shareOfLocalVoiceChange` per keyword).

**API.** Run the Actor and get the results in one call:

```bash
curl -X POST "https://api.apify.com/v2/acts/ocius-data~google-maps-rank-grid/run-sync-get-dataset-items?token=YOUR_API_TOKEN&format=json" \
  -H "Content-Type: application/json" \
  -d '{"businessName": "Expert Dental - Midtown", "keywords": ["dentist"], "gridSize": 5, "spacing": 1}'
```

Python, with the official `apify-client` package:

```python
from apify_client import ApifyClient
client = ApifyClient("YOUR_API_TOKEN")
run = client.actor("ocius-data/google-maps-rank-grid").call(run_input={"businessName": "Expert Dental - Midtown", "keywords": ["dentist"], "gridSize": 7})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    if row["rowType"] == "summary":
        print(row["keyword"], row["averageTotalRank"], row["shareOfLocalVoice"])
        print(row["heatmap"])
```

**Webhooks.** In the task, open **Integrations**, add a webhook for `ACTOR.RUN.SUCCEEDED` and download the dataset from `resource.defaultDatasetId`.

**Google Sheets, Zapier, Make and n8n.** Use the Apify integration or app. Filter rows by `rowType` to keep points and summaries in separate sheets.

**MCP and AI agents.** Add `ocius-data/google-maps-rank-grid` to the Apify MCP server (`https://mcp.apify.com`) and an agent can run a grid with plain inputs such as `{"businessName": "...", "keywords": ["..."]}`.

### Statuses and errors

Every point has a row in the dataset with its `status`. Points that could not be checked get a free status row with `dataQuality: "NOT_CHECKED"` and the reason in `rankExplanation`. The same counts are in the `RUN_SUMMARY` record.

| Status | Meaning | Charged | What to do |
|---|---|---|---|
| `OK` | Google returned businesses at this point; the rank (or "not found") is saved. | Yes | Nothing |
| `NO_RESULTS` | Google lists no business for this keyword at this point (for example over water). The row is saved with `resultsCount: 0`. | No | Nothing, or use a smaller spacing. |
| `BLOCKED` | Google kept blocking after all attempts and proxy changes. A free status row is saved. | No | Run again later. |
| `ERROR` | Unexpected response from Google. A free status row is saved. | No | Open an issue with the run ID. |
| `NOT_PROCESSED` | Your maximum cost per run or the timeout was reached first. | No | Raise the limit. |

`RUN_SUMMARY` also shows the resolved business and centre, the summary of every keyword, the proxy used, warnings (for example "business not found at any point") and the reconciliation between rows saved and events charged.

### Known limits

- **Depth:** ranks 1 to 20 (the first page of Google Maps results). A business below position 20 is reported as not found.
- **Results change with the searcher.** Google ranks local results by the map position and also by where the searcher's IP is, and results move during the day. By default the Actor uses Apify datacenter IPs for the United States and a residential IP in the selected country elsewhere (the same residential IP for the whole grid). Expect small differences between runs; compare trends over several runs, not one point.
- **Zoom** changes how far Google looks around each point. Keep the same zoom between runs. The default (14) is a neighbourhood view of about 7 x 5 km.
- **Name-only matching** is used only when the business cannot be found by name lookup; it requires the exact name as shown on Google Maps.
- **Data:** business listing data only. No reviews, reviewer names, phone numbers or e-mails are collected.

### FAQ

**What is share of local voice?** The percentage of grid points where the business is in the top 3, which is the part of the map that gets most clicks (the local "3-pack").

**What spacing should I use?** Dense cities: 0.5 to 1 km. Suburbs: 1 to 3 km. Service-area businesses: 3 to 8 km.

**Is it legal?** The Actor reads public Google Maps listing data, without login, and collects business information only.

**How do I track a competitor?** Run the same grid with the competitor's name or Maps URL, or enable **Include all businesses per point** to see every business at every point in one run.

### Related Actors from ocius-data

Built to the same standard (status per input in `RUN_SUMMARY`, actionable errors, pay only for valid data), for a complete SEO workflow:

- **[Bulk Keyword Search Volume & CPC - Keyword Planner](https://apify.com/ocius-data/keyword-metrics)**: search volume, CPC and competition for the keywords you track on the grid.
- **[Google Autocomplete API: Keyword Ideas & Questions](https://apify.com/ocius-data/google-autocomplete-keyword-ideas)**: find the local keywords customers actually type ("dentist near me open now").
- **[Bulk Domain Authority & Backlinks Checker API](https://apify.com/ocius-data/domain-authority)**: authority and backlinks of your website and of the competitors found on the grid.

### Changelog

- **November 2026** Fixed: a business name is no longer matched to a different listing that only shares category words (for example "Dental Studio"). When Google's listing name differs from the name you entered, the run now logs a warning with the listing it used.
- **0.1.9** (4 November 2026) Rank change since the previous run at every point and in the summary row (named key-value store, free); per-point `competitorAbove` and `rankExplanation`; quick-win points (rank 4 to 6); colour HTML heatmap (record `HEATMAP`) linked from the summary row and the status message; free status rows for points that could not be checked; `dataQuality` and `warnings` per row; `recordType`, a first **Overview** view, labelled and formatted columns, and output links for every view, CSV and the heatmap. The final charge check now takes under 1 second instead of about 7 (it reads the platform counter only when the SDK counter does not match).
- **0.1.5** (2 November 2026) Fixed a false "charge reconciliation mismatch" warning in RUN_SUMMARY: the final check now waits for the platform charge counter to catch up. Charges themselves were always correct.
- **0.1** (November 2026) First version: 3x3 to 9x9 grids in km or miles, business lookup by name, place ID or Maps URL, centre by coordinates or address, top 3 competitors per point, optional full list of businesses, summary row with average rank, share of local voice, top competitors and text heatmap.

### Tutorials and examples

**Ready-to-import templates:** [Google Sheets](https://github.com/christianbarbosapayan-commits/apify-data-apis-examples/tree/main/integrations/google-sheets), [Make](https://github.com/christianbarbosapayan-commits/apify-data-apis-examples/blob/main/integrations/make.md), [Zapier](https://github.com/christianbarbosapayan-commits/apify-data-apis-examples/blob/main/integrations/zapier.md) and a [webhook receiver](https://github.com/christianbarbosapayan-commits/apify-data-apis-examples/blob/main/integrations/webhook/receiver.mjs).

- Tutorial: [Google Maps rank grid tracker with an API: a local SEO heatmap in JavaScript](https://github.com/christianbarbosapayan-commits/apify-data-apis-examples/blob/main/tutorials/google-maps-rank-grid-local-seo.md)
- Example code: [JavaScript: rank grid heatmap](https://github.com/christianbarbosapayan-commits/apify-data-apis-examples/blob/main/javascript/maps_rank_grid_heatmap.mjs)
- All examples: [apify-data-apis-examples on GitHub](https://github.com/christianbarbosapayan-commits/apify-data-apis-examples)

### Rate this Actor

If this Actor saved you time, please consider leaving a rating or a short review on its Apify Store page. It helps other users find it and shows us what to improve. If something does not work as expected, please open an issue first so we can fix it.

### Support

Open an issue on the Actor page. Issues are answered within one business day.

# Changelog

This Actor's version history is a separate document: https://apify.com/ocius-data/google-maps-rank-grid/changelog.md

# Actor input Schema

## `businessName` (type: `string`):

Name of the business as it appears on Google Maps. The Actor looks it up to get its place ID and location, so the grid can be centred on it automatically. Give at least one of: business name, place ID or Maps URL.

## `mapsUrl` (type: `string`):

Link of the business listing on Google Maps (https://www.google.com/maps/place/... or https://maps.app.goo.gl/...). The most precise way to identify one branch of a chain.

## `placeId` (type: `string`):

Google place ID of the business, starting with "Ch" (for example ChIJgSvKJARZwokRGGDLFY2oPEc). Matching by ID is exact. With a place ID only, also give the grid centre.

## `keywords` (type: `array`):

Search terms to track, as a customer would type them in Google Maps (for example "dentist", "emergency plumber"). From 1 to 10 keywords. Each keyword checks every point of the grid.

## `centerLat` (type: `number`):

Latitude of the grid centre. Leave empty to centre the grid on the business location.

## `centerLng` (type: `number`):

Longitude of the grid centre. Give it together with the centre latitude.

## `centerAddress` (type: `string`):

Address or place to centre the grid on, for example "Times Square, New York". Used when no centre latitude and longitude are given.

## `gridSize` (type: `integer`):

Points per side: 3, 5, 7 or 9. 5 means a 5x5 grid of 25 points. Each point is one charged check per keyword.

## `spacing` (type: `number`):

Distance between neighbouring points, in the unit below. A 5x5 grid with 1 km spacing covers 4 x 4 km.

## `distanceUnit` (type: `string`):

Unit of the spacing: kilometres or miles.

## `zoom` (type: `integer`):

Google Maps zoom level used at each point. 14 is a neighbourhood view (about 7 x 5 km), 13 a district, 15 a few blocks. Use the same zoom between runs to compare results.

## `language` (type: `string`):

Interface language for Google Maps, as a language code (en, es, pt-BR, fr, de...).

## `country` (type: `string`):

Two-letter country code of the Google domain (us, gb, co, mx, es...). Also selects the country of the residential proxy if it is needed.

## `includeAllBusinesses` (type: `boolean`):

Add the full list of up to 20 businesses at each point (name, place ID, rating, reviews count, category, address, coordinates, website). Same price per point.

## `compareWithPreviousRun` (type: `boolean`):

Shows how the rank changed at every point since the last run for the same business, keyword and grid (previousRank, rankChange, changeStatus and the change of the averages in the summary row). The previous ranks are kept in a named key-value store in your account. Free: no extra requests and no extra charge.

## `monitoringStoreName` (type: `string`):

Name of the key-value store (kept between runs) that remembers the ranks of each run. Use the same name in every scheduled run. Letters, numbers and hyphens only.

## `maxConcurrency` (type: `integer`):

Points checked in parallel. Default 8.

## `maxRetries` (type: `integer`):

Attempts per point before it is marked as failed. Failed points are not charged.

## `proxyConfiguration` (type: `object`):

By default: Apify datacenter proxy for the United States, and a residential IP in the selected country elsewhere, because Google ranks local results partly by where the searcher is. The Actor switches proxy automatically if Google blocks requests.

## Actor input object example

```json
{
  "businessName": "Expert Dental - Midtown",
  "keywords": [
    "dentist"
  ],
  "gridSize": 5,
  "spacing": 1,
  "distanceUnit": "km",
  "zoom": 14,
  "language": "en",
  "country": "us",
  "includeAllBusinesses": false,
  "compareWithPreviousRun": true,
  "monitoringStoreName": "maps-rank-grid-history",
  "maxConcurrency": 8,
  "maxRetries": 4,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `overview` (type: `string`):

Every grid point and the summary of each keyword.

## `grid` (type: `string`):

Rank per point, change since the previous run and the business to overtake.

## `summary` (type: `string`):

Average rank, share of local voice, quick wins and change since the previous run.

## `competitors` (type: `string`):

Top 3 businesses per point and the business ranked just above yours.

## `gridCsv` (type: `string`):

The grid points view as a CSV file, ready for Excel or Google Sheets.

## `results` (type: `string`):

Every row with all fields.

## `heatmap` (type: `string`):

Colour heatmap of the rank at every point, one grid per keyword, with the change since the previous run.

## `runSummary` (type: `string`):

Status per point (OK, NO_RESULTS, BLOCKED, ERROR), insights, proxy usage and charge reconciliation.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "businessName": "Expert Dental - Midtown",
    "keywords": [
        "dentist"
    ],
    "gridSize": 5,
    "spacing": 1,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("ocius-data/google-maps-rank-grid").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "businessName": "Expert Dental - Midtown",
    "keywords": ["dentist"],
    "gridSize": 5,
    "spacing": 1,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("ocius-data/google-maps-rank-grid").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "businessName": "Expert Dental - Midtown",
  "keywords": [
    "dentist"
  ],
  "gridSize": 5,
  "spacing": 1,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call ocius-data/google-maps-rank-grid --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,ocius-data/google-maps-rank-grid"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/8cOLIJFbFssZrQzEE/builds/ohvZM8uWE3sny8JEj/openapi.json
