# Google Maps Scraper - Business Leads, Emails & Reviews (`andrew_babo/google-maps-scraper`) Actor

Scrape Google Maps business data at scale: names, addresses, phones, websites, emails, opening hours, ratings and reviews. HTTP-first engine, 10-30x faster than browser scrapers, breaks the 120-result limit. Export to CSV, Excel, JSON.

- **URL**: https://apify.com/andrew\_babo/google-maps-scraper.md
- **Developed by:** [Andrew Babo](https://apify.com/andrew_babo) (community)
- **Categories:** Lead generation, E-commerce, SEO tools
- **Stats:** 33 total users, 21 monthly users, 95.2% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Google Maps Scraper — extract business data, leads and reviews

Scrape Google Maps business listings at scale and export them to **CSV, Excel, JSON or the API**. For any city, region, country or map area you get business names, full addresses, phone numbers, websites, emails, categories, opening hours, ratings, review counts, star histograms, photos and individual reviews.

Built on an **HTTP-first engine** that is ~10–30× faster than browser-based Google Maps scrapers, with adaptive anti-blocking and quadtree tiling that breaks Google's ~120-results-per-viewport limit. Cost is roughly **$0.03 per 1,000 places**.

### What you can use it for

- **Lead generation** — build B2B prospect lists of local businesses with phone, website and email.
- **Market and competitor research** — map every competitor in an area with ratings and review volume.
- **Review monitoring** — pull reviews and star distribution for a brand or a whole category.
- **Local SEO audits** — check listings, categories, opening hours and contact data at scale.
- **Data enrichment** — hydrate place IDs, CIDs or Google Maps URLs into full records.

### How to scrape Google Maps with this actor

1. Enter one or more search terms in `searchStringsArray` (e.g. `["coffee shop", "dentist"]`).
2. Set `locationQuery` to a city, region or country — or pass `bbox`, `centerPoint` + `radiusMeters`, or a GeoJSON `customGeolocation`.
3. Optionally set `maxCrawledPlacesPerSearch`, `maxReviews`, `maxImages` and `scrapeContacts` (emails + social links).
4. Run the actor and download the dataset as CSV, Excel, JSON, XML or read it through the Apify API.

### FAQ

**Can I scrape emails from Google Maps?** Google Maps itself does not publish emails. With `scrapeContacts: true` the actor visits each business website and extracts `emails[]` and `socialLinks{}` from it.

**Does it get more than 120 results?** Yes. Google returns only ~120 places per viewport, so the actor tiles the area adaptively and deduplicates by `placeId`, returning thousands of places for a metro area.

**Do I need proxies?** The HTTP tier rarely needs residential proxies. Any Apify proxy group works; bandwidth stays tiny because no images or JavaScript are downloaded.

**Is it free?** The actor itself is free to use — you only pay Apify platform compute, around $0.03 per 1,000 places, so the free plan covers a large volume.

**Is scraping Google Maps legal?** This actor collects publicly visible business information. Review data contains personal data, so only enable `maxReviews` when you need it and comply with GDPR and Google's terms in your jurisdiction.

### How it works

1. **HTTP tier (default)** — calls the same internal JSON endpoints the Maps frontend uses (`/search?tbm=map` for listings, `/maps/preview/place` for details, and an internal BOQ proxy for reviews). No browser, no rendering: tens of ms per request.
2. **Browser stealth tier (fallback)** — when the HTTP tier detects a block (`/sorry/`, consent wall, 429/403), the affected tile automatically re-runs through a stealth Playwright browser (`engine: "auto"`).
3. **Adaptive quadtree tiling** — `locationQuery` (or a GeoJSON `customGeolocation`) is geocoded via OpenStreetMap, then the area is probed tile by tile; any tile returning a dense result set (≥18/20 per page) is split into 4 sub-tiles. This yields far more than Google's ~120-result cap while saving requests on sparse areas.
4. **AIMD adaptive concurrency** — concurrency grows while requests succeed and halves on every block signal, keeping block-rate near zero without fixed rate limits.
5. **Dedup by `placeId`** — overlapping tiles never produce duplicate rows.

### Input

| Field | Type | Description |
|---|---|---|
| `searchStringsArray` | string\[] | Queries, e.g. `["coffee shop", "restaurant"]` |
| `locationQuery` | string | City/region/country, geocoded via OSM (e.g. `"New York, USA"`) |
| `customGeolocation` | GeoJSON | Point (5 km radius), Polygon or MultiPolygon — overrides `locationQuery` |
| `maxCrawledPlacesPerSearch` | number | Cap per search string (0 = unlimited) |
| `language` / `countryCode` | string | `hl` / `gl`, e.g. `en` / `us` (defaults) |
| `categoryFilterWords` | string\[] | Keep only places whose category contains any word |
| `placeMinimumStars` | string | Keep only places with rating ≥ value |
| `skipClosedPlaces` | boolean | Skip permanently closed places (default true) |
| `scrapePlaceDetailPage` | boolean | Enrich with address/phone/website/hours (default true; false = listing-only, fastest) |
| `maxReviews` | number | Reviews per place (0 = none), 10 per page |
| `reviewsSort` | string | `most_relevant` | `newest` | `highest_rating` | `lowest_rating` |
| `reviewsStartDate` | string | ISO date; older reviews skipped (with `newest` sort, stops early) |
| `maxImages` | number | Max photo URLs per place (0 = none) |
| `startUrls` | array | Direct Google Maps place URLs |
| `placeIds` | string\[] | Direct place IDs (`ChIJ...` or `0x...:0x...`) |
| `engine` | string | `auto` (default) | `http` | `browser` |
| `maxConcurrency` | number | Upper bound for the adaptive controller |
| `failOnZeroResults` | boolean | Fail the run when 0 places were pushed while blocks were detected (default true) — prevents silent-empty runs |
| `deepRecall` | boolean | More aggressive tiling (lower split threshold, deeper quadtree) for maximum recall |
| `includeRunSummary` | boolean | Push a final `__runSummary` item with counts and field coverage |
| `mode` | string | `ids` (identifiers only) | `basic` (listing fields, no detail request) | `full` (default) |
| `bbox` | number\[] | `[south, west, north, east]` — explicit area, overrides `locationQuery` |
| `centerPoint` + `radiusMeters` | number\[] / number | Area around a point — `[lat, lng]`, **latitude first** (a `{lat,lng}` object is also accepted through the API). Wrong order fails fast with `INVALID_COORDINATES`; a malformed `bbox` fails with `INVALID_BBOX`. Default radius 5000 m |
| `gridStepMeters` | number | Seed grid cell size. Leave at `0` (default) to derive it from the area: one Google viewport only ever returns ~120 places, so metro/province/country areas are gridded automatically (and the grid is widened if it would exceed the request budget) |
| `cids` | string\[] | Decimal Google CIDs to hydrate into full place records |
| `scrapeContacts` | boolean | Fetch each place website and extract `emails` + `socialLinks` (1 extra request per place) |
| `speedMode` | string | `turbo` (default, fastest with full coverage) | `fast` | `balanced` | `accurate` |
| `scrapeReviewsDistribution` | boolean | Rescue the star histogram from the review feed when the place payload omits it (default true) |
| `requestTimeoutSecs` | number | Per-request timeout (default 30) |
| `maxRunTimeSecs` | number | Hard wall-clock budget for the crawl (default 3600) |

### Output

One dataset item per place (field names mirror `compass/crawler-google-places` for drop-in compatibility):

`title`, `placeId`, `fid`, `cid`, `categoryName`, `categories[]`, `address`, `street`, `city`, `state`, `postalCode`, `countryCode`, `location.lat/lng`, `plusCode`, `totalScore`, `reviewsCount`, `reviewsDistribution`, `openingHours[]` (`{day, hours}`), `website`, `phone`, `imageUrls[]`, `reviews[]` (`reviewId`, `name`, `profilePhotoUrl`, `reviewerUrl`, `reviewerId`, `stars`, `publishedAtDate`, `text`, `textLanguage`, `likesCount`, `responseFromOwnerText`, `reviewImageUrls[]`), `url`, `searchString`, `rank`, `scrapedAt`.

Every item on every branch (search, `bbox`, grid, `cids`, `startUrls`) carries the full compass field set — unknown values are `null`, never missing keys — plus `categoryMatched` (`true`/`false` with `categoryFilterWords`, otherwise `null`).

Every item also carries `datasetSchemaVersion`, `detailLevel` (`ids`/`basic`/`full`), `matchedSearchStrings`, `language`, `searchCountryCode`, and — with `scrapeContacts` — `emails[]` and `socialLinks{}`.

#### Error codes

The run fails with a machine-readable prefix in the status message and `errorCode` in `STATS`: `NO_INPUT`, `INVALID_LOCATION`, `INVALID_COORDINATES`, `INVALID_BBOX`, `GEOCODE_FAILED`, `INVALID_CID`, `ALL_BLOCKED`, `NO_RESULTS`, `NO_RESULTS_IN_AREA`.

#### Geo accuracy (0.8)

Google ignores a viewport that is too wide or covers a sparse area and answers from the egress IP instead, which used to leak places from the proxy's country into the dataset. The actor now:

- grids every area (tiles never exceed 15 km) and derives the zoom from the tile span instead of a fixed value;
- skips tiles that do not intersect the geocoded admin polygon;
- drops records outside the requested area at collection time, so the `maxCrawledPlacesPerSearch` budget is spent on real results;
- reports `outOfArea`, `inAreaPct` and `ipLeakPages` in `STATS` / `__runSummary`, and fails with `NO_RESULTS_IN_AREA` when nothing inside the area survived.

`includeRunSummary` now defaults to `true`.

#### Cost

Measured on live runs: ~500 places in 44 s on 4 GB, i.e. roughly **$0.003 per 100 places** (~$0.03 per 1,000 places) of Apify compute at the default settings. `mode: "basic"` or `scrapePlaceDetailPage: false` cuts that roughly in half; `maxReviews` and `scrapeContacts` add one request per place.

Run statistics are stored in the key-value store under `STATS`: searched/listed/enriched/pushed/blocked counts, `degraded` flag, and per-field `coverage` percentages.

#### Data source: listing-first

The listing response already carries address parts, opening hours, E.164 phone, images, categories and coordinates, so the actor pushes listing records directly and only fetches the place detail page when a record is incomplete. Measured coverage on 160 live places (SG/VN/AU/MY): title 100%, placeId/cid 100%, location 100%, categoryName 100%, address 99%, city 99%, countryCode 100%, openingHours 96%, website 91%, phone/phoneUnformatted 94%, imageUrl 88%, postalCode 88%, totalScore 88%, state 74%.

### Performance notes

- Listing page (20 places): ~0.5 s. Place detail: ~0.15 s. 10 reviews: ~0.6 s.
- `scrapePlaceDetailPage: false` gives the cheapest, fastest listing-only crawl.
- Residential proxies are rarely needed for the HTTP tier; if you enable `proxyConfiguration`, bandwidth stays tiny because no images/JS are downloaded.
- For very large jobs, run several instances in parallel with different `locationQuery` areas instead of one huge run.

### Known limitations

- The internal `pb` formats have no stability contract; parsers fail safe (null fields, not crashes) and `engine: "auto"` falls back to the browser if Google changes a response shape.
- Reviews include reviewer profile data — only enable `maxReviews` when you actually need it (personal data).
- Partial runs are labelled: when `maxRunTimeSecs` or the tile budget is reached, the run still succeeds but `STATS`/`__runSummary` set `partial: true`, `timedOut`, and `degraded` so a pipeline can tell a partial sweep from a complete one.
- `reviewsDistribution` (star histogram) **is supported over plain HTTP** since 0.9.16 and reaches 98-100% coverage since 1.5 (places with zero reviews get an exact all-zero histogram). The breakdown lives at `[175][3]` of the place record, but Google only serves it when the `pb` carries the `!14m2!7e81!15i10112` group before `!15m106` (94 KB payload without it and `[175] = null`; 249 KB with it and the real `[1★..5★]` counts). No session token or browser is needed — verified against a live browser capture and reproduced from Apify IPs. When a record still comes back lean, `scrapeReviewsDistribution: true` derives the counts from the full review feed, and only when the whole feed was fetched.
- `reviewsCount` and `plusCode` are best-effort: Google only serves them for a subset of records on the internal endpoints, so expect partial coverage. Use `maxReviews` if you need review data itself.
- `openingHours` from the listing tier covers the current week's day entries; historical/special hours are not exposed.
- Not affiliated with Google. You are responsible for complying with applicable terms and privacy laws (GDPR, ...).

# Actor input Schema

## `searchStringsArray` (type: `array`):

Queries to search on Google Maps, e.g. \["coffee shop", "restaurant"]. Combined with locationQuery or customGeolocation.

## `locationQuery` (type: `string`):

Free-form location (city, region, country) geocoded via OpenStreetMap, e.g. "New York, USA". The area is automatically split into adaptive tiles to bypass the ~120 results per viewport limit.

## `customGeolocation` (type: `object`):

Optional GeoJSON Point/Polygon/MultiPolygon (or {type:'Feature', geometry: ...}). Overrides locationQuery. Point uses a 5 km radius by default.

## `maxCrawledPlacesPerSearch` (type: `integer`):

Maximum number of places to scrape per search term. 0 or empty = unlimited.

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

Google Maps interface language (hl param), e.g. en, es, vi.

## `countryCode` (type: `string`):

Country for the Google geo parameter (gl param), lowercase ISO code, e.g. us, gb, sg.

## `categoryFilterWords` (type: `array`):

Keep only places whose categories contain at least one of these words (case-insensitive). Empty = keep all.

## `placeMinimumStars` (type: `string`):

Keep only places with rating at least this value. Empty = no filter.

## `skipClosedPlaces` (type: `boolean`):

Exclude places marked as permanently closed.

## `scrapePlaceDetailPage` (type: `boolean`):

Visit each place's detail endpoint to enrich with phone, website, opening hours, photos, additional info. Disable for maximum speed (listing data only).

## `maxReviews` (type: `integer`):

Maximum reviews to scrape per place (0 = none). Reviews are fetched 10 per page.

## `reviewsSort` (type: `string`):

Sort order for scraped reviews.

## `reviewsStartDate` (type: `string`):

Optional ISO date (YYYY-MM-DD). Only keep reviews published after this date.

## `maxImages` (type: `integer`):

Maximum image URLs to collect per place (0 = none). Images come from the detail payload.

## `startUrls` (type: `array`):

Direct Google Maps place/search URLs to scrape instead of searchStringsArray.

## `placeIds` (type: `array`):

Direct Google place IDs (ChIJ... or 0x...:0x... CID pairs) to scrape details for, without searching.

## `engine` (type: `string`):

auto = HTTP-first with browser fallback on blocks/parse failures; http = HTTP only (fastest); browser = Playwright stealth only (slowest, most compatible).

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

Maximum parallel HTTP requests. Adaptive controller (AIMD) may lower this automatically when blocks are detected. Leave empty to let Speed mode decide.

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

Apify Proxy configuration. Residential proxies recommended for large runs. Not required for small runs.

## `failOnZeroResults` (type: `boolean`):

When true (default) the run FAILS instead of silently succeeding if 0 places were scraped — e.g. when every listing request was blocked.

## `deepRecall` (type: `boolean`):

Split the map area into more, smaller tiles for maximum coverage. Slower and uses more requests, still far cheaper than browser crawlers. Use when you need every place in an area.

## `includeRunSummary` (type: `boolean`):

Push one extra item containing run statistics and field-coverage percentages at the end of the dataset.

## `detailMode` (type: `string`):

always = always fetch the place detail payload (best field coverage), auto = only when the listing record is incomplete, never = listing only (fastest/cheapest).

## `richDetailAttempts` (type: `integer`):

How many times to retry a place detail request while Google withholds reviewsCount / the full 7-day opening hours. Higher = better coverage, slightly more requests. Leave empty to let Speed mode decide.

## `detailFanout` (type: `integer`):

How many detail attempts run in parallel, each on its own warm session. Higher = faster on lean shards, more requests in flight. Leave empty to let Speed mode decide.

## `richListingAttempts` (type: `integer`):

Google returns a search page either without or with review counts and full opening hours. Retry the listing request up to this many times to land a rich page (cheap: one retry enriches ~20 places at once). Leave empty to let Speed mode decide.

## `dedupeByPlaceId` (type: `boolean`):

Remove places already scraped by an earlier search term. Each item lists every matching term in matchedSearchStrings.

## `mode` (type: `string`):

ids = identifiers only (cheapest), basic = listing fields without detail requests, full = listing + detail enrichment.

## `bbox` (type: `array`):

\[south, west, north, east] in degrees. Overrides locationQuery.

## `centerPoint` (type: `array`):

Center of the search area as \[lat, lng] (latitude first) or {"lat":..,"lng":..}. Used with radiusMeters. Wrong order fails the run with INVALID\_COORDINATES.

## `radiusMeters` (type: `integer`):

Radius in meters around centerPoint.

## `gridStepMeters` (type: `integer`):

Seed grid cell size. Leave at 0 to let the actor pick one from the area size (a single viewport only returns ~120 places, so large areas are gridded automatically).

## `cids` (type: `array`):

Decimal Google CIDs to hydrate into full place records.

## `scrapeContacts` (type: `boolean`):

Fetch each place website and extract emails + social links (1 extra request per place).

## `requestTimeoutSecs` (type: `integer`):

Timeout for each Google request in seconds.

## `maxRunTimeSecs` (type: `integer`):

Hard wall-clock budget for the crawl in seconds.

## `scrapeReviewsDistribution` (type: `boolean`):

Rescue the star histogram from the review feed when the place payload did not carry it (only for places with few enough reviews to count exactly). Turn off to save requests.

## `seedByCities` (type: `boolean`):

Add extra seed tiles for towns/districts inside the searched area. Improves recall in rural areas.

## `debugDumpPayload` (type: `integer`):

Store the raw place-detail payload for the first N places in the key-value store (debugging only).

## `reviewsDistributionMaxReviews` (type: `integer`):

Only derive reviewsDistribution when the place has at most this many reviews (the whole feed must be pulled to be exact).

## `speedMode` (type: `string`):

turbo = default: fastest wall-clock with full field coverage; fast = fewer retries; balanced = more listing retries; accurate = maximum retries.

## `maxPlacesPerTile` (type: `integer`):

Cap on how many places a single map tile may contribute in one wave. Lower spreads results across the whole area; higher favours dense centres.

## Actor input object example

```json
{
  "searchStringsArray": [
    "coffee shop"
  ],
  "locationQuery": "New York, USA",
  "maxCrawledPlacesPerSearch": 100,
  "language": "en",
  "countryCode": "us",
  "categoryFilterWords": [],
  "placeMinimumStars": "",
  "skipClosedPlaces": true,
  "scrapePlaceDetailPage": true,
  "maxReviews": 0,
  "reviewsSort": "newest",
  "reviewsStartDate": "",
  "maxImages": 10,
  "startUrls": [],
  "placeIds": [],
  "engine": "auto",
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "failOnZeroResults": true,
  "deepRecall": false,
  "includeRunSummary": true,
  "detailMode": "auto",
  "dedupeByPlaceId": true,
  "mode": "full",
  "bbox": [],
  "centerPoint": [],
  "radiusMeters": 5000,
  "gridStepMeters": 0,
  "cids": [],
  "scrapeContacts": false,
  "requestTimeoutSecs": 30,
  "maxRunTimeSecs": 3600,
  "scrapeReviewsDistribution": true,
  "seedByCities": true,
  "debugDumpPayload": 0,
  "reviewsDistributionMaxReviews": 200,
  "speedMode": "turbo",
  "maxPlacesPerTile": 24
}
```

# Actor output Schema

## `STATS` (type: `string`):

No description

# 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 = {
    "searchStringsArray": [
        "coffee shop"
    ],
    "locationQuery": "New York, USA",
    "language": "en",
    "countryCode": "us",
    "bbox": [],
    "centerPoint": [],
    "cids": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("andrew_babo/google-maps-scraper").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 = {
    "searchStringsArray": ["coffee shop"],
    "locationQuery": "New York, USA",
    "language": "en",
    "countryCode": "us",
    "bbox": [],
    "centerPoint": [],
    "cids": [],
}

# Run the Actor and wait for it to finish
run = client.actor("andrew_babo/google-maps-scraper").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 '{
  "searchStringsArray": [
    "coffee shop"
  ],
  "locationQuery": "New York, USA",
  "language": "en",
  "countryCode": "us",
  "bbox": [],
  "centerPoint": [],
  "cids": []
}' |
apify call andrew_babo/google-maps-scraper --silent --output-dataset

```

## MCP server setup

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

```

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/bamPFugWexFtIIJW0/builds/ttbWwgdR1ISvARCFy/openapi.json
