# Google Maps Scraper (`abotapi/google-maps-scraper`) Actor

Extract business data from Google Maps at scale. Get names, addresses, phone numbers, websites, ratings, reviews, opening hours, popular times, photos, and 40+ data points per listing.

- **URL**: https://apify.com/abotapi/google-maps-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Lead generation, Social media, Real estate
- **Stats:** 85 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.70 / 1,000 results

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?

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 Business Scraper

![Google Maps Business Scraper](https://i.postimg.cc/dtWn9qwr/banner.png)

Extract business data from Google Maps at scale. Get names, addresses, phone numbers, websites, ratings, reviews, opening hours, popular times, photos, and 40+ data points per listing.

### What Data Can You Extract?

#### Business Information

- Business name, category, subcategories
- Full address with street, city, state, postal code, country
- Phone number and website URL
- Google Place ID and Maps URL

#### Ratings & Reviews

- Average rating (1-5 stars)
- Total review count
- Individual reviews with text, rating, date, author
- Owner responses to reviews

#### Location Data

- Latitude and longitude coordinates
- Plus Code (Google's location code)
- "Located in" parent location (e.g., mall, airport)

#### Operational Details

- Opening hours (daily schedule)
- Popular times (hourly occupancy by day)
- Temporarily/permanently closed status
- Price level ($, $$, $$$, $$$$)

#### Rich Content

- Photo URLs (configurable limit)
- Menu URL
- Order/delivery links (DoorDash, UberEats, etc.)
- "People also search" related businesses

#### Search Context

- Search query that found the business
- Rank in search results
- Sponsored/advertisement indicator

### Quick Start

#### Basic Search

```json
{
  "searchStringsArray": ["restaurants"],
  "location": "New York, NY",
  "maxResultsPerSearch": 100
}
```

#### Search by Coordinates

```json
{
  "searchStringsArray": ["coffee shops"],
  "lat": 40.7128,
  "lng": -74.0060,
  "zoom": 15,
  "maxResultsPerSearch": 50
}
```

#### Large Area with Grid Search

```json
{
  "searchStringsArray": ["hotels"],
  "customGeolocation": {
    "type": "Polygon",
    "coordinates": [[[-122.5, 37.7], [-122.3, 37.7], [-122.3, 37.85], [-122.5, 37.85], [-122.5, 37.7]]]
  },
  "zoom": 16,
  "maxResultsPerSearch": 200
}
```

#### With Reviews

```json
{
  "searchStringsArray": ["dentists"],
  "location": "Chicago, IL",
  "maxResultsPerSearch": 50,
  "maxReviews": 20,
  "reviewsSort": "newest"
}
```

#### Direct Place ID Lookup

```json
{
  "searchStringsArray": ["place_id:ChIJN1t_tDeuEmsRUsoyG83frY4"]
}
```

### Input Parameters

#### Search Options

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `searchStringsArray` | string\[] | `["restaurants"]` | Search queries or Place IDs (`place_id:ChIJ...`) |
| `startUrls` | object\[] | `[]` | Direct Google Maps URLs to scrape |
| `location` | string | `"New York, NY"` | Location text appended to search |

#### Geographic Targeting

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `lat` | number | - | Latitude for coordinate search (-90 to 90) |
| `lng` | number | - | Longitude for coordinate search (-180 to 180) |
| `customGeolocation` | object | - | GeoJSON Polygon/MultiPolygon for grid search |
| `zoom` | integer | `14` | Map zoom level (1-21). Higher = smaller grid cells |
| `country` | string | - | Country filter |
| `state` | string | - | State/region filter |
| `city` | string | - | City filter |
| `postalCode` | string | - | Postal/ZIP code filter |

#### Extraction Limits

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `maxResultsPerSearch` | integer | `5` | Max results per search query (up to 10,000) |
| `maxReviews` | integer | `0` | Max reviews per business (0 = skip reviews) |
| `maxImages` | integer | `10` | Max photo URLs per business |

#### Send results into your apps (MCP connectors)

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape — the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human-readable **summary** of each record — not the full JSON. Each item becomes one entry with a **title** and its key fields flattened to plain text. The **complete record always stays in the Apify dataset**.

1. Authorize a connector once under **Apify → Settings → Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. (If the picker is empty, you haven't authorized a connector yet.)
3. For **Notion**, also set `notionParentPageUrl` to the page where items should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

### Output Options

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `reviewsSort` | string | `"relevant"` | Sort reviews: `relevant`, `newest`, `highest`, `lowest` |
| `skipClosedPlaces` | boolean | `false` | Skip closed businesses |
| `oneReviewPerRow` | boolean | `false` | Output one row per review (for CSV) |
| `language` | string | `"en"` | Language code for results |

#### Performance

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `maxConcurrency` | integer | `5` | Parallel browser pages (1-20) |
| `proxyConfiguration` | object | - | Apify proxy settings |

#### Resume and recurring updates

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `resumeFromRunId` | string | - | Run ID to continue an interrupted run. Loads that run's already-scraped Place IDs and skips them. |
| `resumeRunId` | string | - | **Deprecated** — use `resumeFromRunId`. Still honored for existing saved tasks. If both are set, `resumeFromRunId` wins. |
| `incrementalMode` | boolean | `false` | Turn on recurring monitoring of the same search. The Actor remembers the previous run's results itself and classifies every business as `NEW`/`UPDATED`/`UNCHANGED`/`REAPPEARED`/`EXPIRED`, so a scheduled re-run reports only what changed. Off by default — no existing user is affected until they opt in. |
| `stateKey` | string | - | Manually name the `incrementalMode` baseline (e.g. to share state across two scheduled tasks, or split one search into separate campaigns). Leave empty to derive it automatically from `searchStringsArray`, `startUrls`, `location`, `lat`/`lng`, `country`/`state`/`city`/`postalCode`, `customGeolocation`, `zoom`, `language`, `maxReviews`, `reviewsSort`, and `skipClosedPlaces` — two differently-configured searches never share a baseline. |
| `emitUnchanged` | boolean | `false` | Also output businesses with no changes since the last run (`changeType: "UNCHANGED"`). **Off by default so a recurring run only bills what's new — turning this on bills and returns an extra row for every unchanged business, every run.** |
| `emitExpired` | boolean | `false` | Also output a synthetic row (`changeType: "EXPIRED"`) for a previously-tracked business no longer found. Only fires after a run proves it fully re-scanned the search (no result cap hit, no CAPTCHA/block, not a resume) — a partial run never marks anything expired. **Off by default — turning this on bills and returns an extra row for every business that disappears.** |

`resumeFromRunId` continues ONE interrupted run. `incrementalMode` is a different, ongoing workflow: point a scheduled Task at the same search with `incrementalMode: true`, and every run after the first reports only `NEW`/`UPDATED`/`REAPPEARED`/`EXPIRED` businesses (plus `UNCHANGED` ones if `emitUnchanged` is on).

With `oneReviewPerRow` on, incremental classification happens per **business**, not per row: a suppressed (`UNCHANGED`, `emitUnchanged` off) business pushes **zero** rows — never a partial set of its review-rows.

### Output Schema

Each business is saved with the following structure:

```json
{
  "placeId": "ChIJ49sQuK-AhYARM6U646SA12Y",
  "title": "Example Coffee Roasters",
  "url": "https://www.google.com/maps/place/...",
  "categoryName": "Coffee shop",
  "address": "100 Example St, San Francisco, CA 94102",
  "street": "100 Example St",
  "city": "San Francisco",
  "state": "CA",
  "postalCode": "94102",
  "countryCode": "US",
  "phone": "+1 555-010-0100",
  "website": "https://www.example.com/",
  "totalScore": 4.5,
  "reviewsCount": 1842,
  "priceLevel": "$$",
  "location": {
    "lat": 37.7763,
    "lng": -122.4158
  },
  "plusCode": "QHFJ+8R Hayes Valley, San Francisco, CA",
  "openingHours": [
    { "day": "Monday", "hours": "7 AM - 6 PM" },
    { "day": "Tuesday", "hours": "7 AM - 6 PM" }
  ],
  "popularTimes": [
    {
      "day": "Monday",
      "hours": [
        { "hour": 7, "occupancyPercent": 20 },
        { "hour": 8, "occupancyPercent": 45 },
        { "hour": 9, "occupancyPercent": 65 }
      ]
    }
  ],
  "imageUrls": ["https://lh3.googleusercontent.com/..."],
  "peopleAlsoSearch": [
    { "title": "Sample Espresso Bar", "placeId": "ChIJ...", "url": "..." }
  ],
  "menuUrl": "https://www.example.com/menu",
  "orderLinks": [
    { "platform": "DoorDash", "url": "https://.../" }
  ],
  "temporarilyClosed": false,
  "permanentlyClosed": false,
  "searchString": "coffee shops",
  "rank": 3,
  "isAdvertisement": false,
  "scrapedAt": "2025-12-06T10:30:00.000Z",
  "changeType": "UPDATED",
  "changedFields": ["totalScore", "reviewsCount"],
  "firstSeenAt": "2025-11-01T08:00:00.000Z",
  "lastSeenAt": "2025-12-06T10:30:00.000Z",
  "reviews": [
    {
      "name": "John D.",
      "text": "Great coffee and atmosphere!",
      "rating": 5,
      "date": "2 weeks ago",
      "responseFromOwner": "Thanks for visiting!"
    }
  ]
}
```

### Scaling to Millions of Records

Google Maps limits search results to ~120-400 per query. To scrape millions of businesses:

#### Strategy 1: Grid Search (Recommended)

Use `customGeolocation` to define a polygon. The scraper auto-generates grid points based on zoom level:

| Zoom | Grid Cell Size | Example: California |
|------|----------------|---------------------|
| 14 | ~6 km | ~2,500 cells |
| 15 | ~3 km | ~10,000 cells |
| 16 | ~1.5 km | ~40,000 cells |

Each cell runs a separate search, so:

- 10,000 cells × 200 results/cell = **2 million potential results**

```json
{
  "searchStringsArray": ["restaurants"],
  "customGeolocation": {
    "type": "Polygon",
    "coordinates": [[[...california boundary...]]]
  },
  "zoom": 15,
  "maxResultsPerSearch": 200
}
```

#### Strategy 2: Multiple Search Terms

Different queries return different businesses:

```json
{
  "searchStringsArray": [
    "restaurants", "cafes", "bars", "hotels",
    "gyms", "dentists", "plumbers", "lawyers"
  ],
  "location": "Los Angeles, CA"
}
```

#### Strategy 3: Neighborhood Batching

Run separate Actor calls per area:

```bash
## Run 1: Downtown
{"searchStringsArray": ["restaurants"], "location": "Downtown, Los Angeles"}

## Run 2: Hollywood
{"searchStringsArray": ["restaurants"], "location": "Hollywood, Los Angeles"}
```

### Resume & Recovery

#### Automatic Protection

- **Deduplication**: Place IDs saved to persistent storage
- **Incremental saves**: Results pushed immediately (never lost)
- **Migration handling**: State saved when Apify migrates servers

#### Manual Resume

If a run crashes, start a new run with `resumeFromRunId`:

```json
{
  "searchStringsArray": ["restaurants"],
  "location": "San Francisco, CA",
  "resumeFromRunId": "abc123xyz"
}
```

#### Recurring monitoring (Incremental mode)

To track the same search over time and get only what changed on each scheduled run:

```json
{
  "searchStringsArray": ["restaurants"],
  "location": "San Francisco, CA",
  "incrementalMode": true
}
```

The first run reports everything as `NEW`. Every run after that (same input, run on a schedule) reports only `UPDATED`/`REAPPEARED`/`EXPIRED` businesses — add `"emitUnchanged": true` to also see `UNCHANGED` ones, and `"emitExpired": true` to see businesses that dropped out of the results (only once a run confirms it fully re-scanned the search).

The scraper loads Place IDs from the previous run and skips already-scraped businesses.

### Anti-Detection Features

Built-in protections for reliable scraping:

- Browser fingerprint rotation
- Webdriver detection masking
- Human-like scrolling with random delays
- Rate limiting (30 requests/minute)
- CAPTCHA detection with exponential backoff
- Automatic retry with session rotation

**Tip**: For high-volume scraping, use residential proxies via `proxyConfiguration`.

### Performance

| Metric | Typical Value |
|--------|---------------|
| Speed | 800-1,200 listings/hour |
| Memory | < 1 GB |
| Concurrency | 1-20 parallel pages |
| Retry attempts | 3 per failed request |

### Export Formats

Download from Apify Dataset:

- **JSON** - Full nested structure
- **CSV** - Flattened for spreadsheets
- **Excel** - .xlsx format
- **XML** - For integrations

### Use Cases

- **Lead Generation**: Build prospect lists with contact info
- **Market Research**: Analyze competitors, pricing, ratings
- **Local SEO**: Track rankings and reviews
- **Real Estate**: Map amenities near properties
- **Investment**: Assess retail density and competition
- **Data Enrichment**: Add location data to existing records

# Actor input Schema

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

List of search queries (e.g., 'restaurants', 'coffee shops')

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

Direct Google Maps URLs to scrape

## `location` (type: `string`):

Location text (e.g., 'New York, NY', 'Tokyo, Japan')

## `lat` (type: `number`):

Latitude for coordinate-based search

## `lng` (type: `number`):

Longitude for coordinate-based search

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

Country for geolocation filtering

## `state` (type: `string`):

State or region for geolocation filtering

## `city` (type: `string`):

City for geolocation filtering

## `postalCode` (type: `string`):

Postal/ZIP code for geolocation filtering

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

GeoJSON geometry (Polygon, MultiPolygon, or Point with radius)

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

Map zoom level (1=world, 21=building). Higher = more precise, fewer results

## `maxResultsPerSearch` (type: `integer`):

Maximum number of results to extract per search query

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

Maximum reviews to extract per business. Set to 0 to disable review extraction (faster)

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

Maximum number of image URLs to extract per business

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

How to sort reviews when extracting

## `oneReviewPerRow` (type: `boolean`):

Output each review as a separate Dataset row (useful for CSV export)

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

Skip temporarily or permanently closed businesses

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

Language code for search results (e.g., 'en', 'es', 'ja')

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

Number of parallel browser contexts (higher = faster but more memory)

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

Apify Proxy settings. Residential proxies recommended for high volume

## `resumeFromRunId` (type: `string`):

Run ID of a previous run to continue. Loads its already-scraped Place IDs and skips them, so this run only appends new businesses. See also Incremental mode below for recurring monitoring instead of a one-off resume.

## `resumeRunId` (type: `string`):

Deprecated — use "Resume From Run ID" above instead. Kept working so existing saved tasks that pass this field name are unaffected. If both are set, "Resume From Run ID" wins.

## `incrementalMode` (type: `boolean`):

Remember this search's results across scheduled runs and classify every business as NEW / UPDATED / UNCHANGED / REAPPEARED / EXPIRED, instead of re-scraping from scratch. State is kept by the Actor itself, keyed on "State key" below (or a hash of your search settings). Default off — no existing user is affected until they opt in.

## `stateKey` (type: `string`):

Optional. Manually name the Incremental-mode baseline so you can control exactly which runs share state (e.g. two different scheduled tasks on the same search). Leave empty to derive it automatically from your search settings — two differently-configured searches then never share a baseline.

## `emitUnchanged` (type: `boolean`):

When Incremental mode is on, also output businesses whose data has not changed since the last run (changeType="UNCHANGED"). Off by default so a recurring run only bills and returns what's new. Turning this on bills and returns an extra row for every unchanged business, every run.

## `emitExpired` (type: `boolean`):

When Incremental mode is on, also output a synthetic row (changeType="EXPIRED") for a previously-tracked business that no longer appears in the search. Only fires after a run proves it fully re-scanned the search (no result cap hit, no CAPTCHA/block, not a resume) — a partial run never marks anything expired. Off by default. Turning this on bills and returns an extra row for every business that disappears.

## `mcpConnectors` (type: `array`):

Optionally send the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify → Settings → Integrations, then select it here. The connector receives a condensed, human-readable summary per item (title + key fields), not the full JSON — the complete record stays in the dataset. Leave empty to skip. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL (or id) of the Notion page under which item pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on items written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "searchStringsArray": [
    "restaurants"
  ],
  "location": "New York, NY",
  "zoom": 14,
  "maxResultsPerSearch": 2,
  "maxReviews": 0,
  "maxImages": 10,
  "reviewsSort": "relevant",
  "oneReviewPerRow": false,
  "skipClosedPlaces": false,
  "language": "en",
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `businesses` (type: `string`):

Dataset containing all scraped business listings with full details.

# 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": [
        "restaurants"
    ],
    "location": "New York, NY",
    "maxResultsPerSearch": 2,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/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": ["restaurants"],
    "location": "New York, NY",
    "maxResultsPerSearch": 2,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/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": [
    "restaurants"
  ],
  "location": "New York, NY",
  "maxResultsPerSearch": 2,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call abotapi/google-maps-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/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/J0lyaOB3ZvycpCtMp/builds/ayUHraPOe4fW40mo2/openapi.json
