# Fast Google Maps Scraper (`solutionssmart/fast-google-map-scraper`) Actor

HTTP-first Google Maps scraper for search listings, place details, reviews, lead scoring, and optional website enrichment.

- **URL**: https://apify.com/solutionssmart/fast-google-map-scraper.md
- **Developed by:** [Solutions Smart](https://apify.com/solutionssmart) (community)
- **Categories:** Lead generation, Automation, Travel
- **Features:** Limited permissions
- **Stats:** 12 total users, 2 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.20 / 1,000 place listeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## Fast Google Maps Scraper

A lightweight Google Maps lead scraper built for low-cost prospecting. It uses a fast HTTP-first pipeline, optional deep details, and built-in opportunity scoring so you do not spend browser compute on every lead.

### What it does

- `list` mode searches Google Maps and saves lightweight listing rows only. This is the cheapest mode.
- `place` mode scrapes one known Google Maps `placeId`.
- `search` mode searches, fetches full place details, and optionally fetches reviews.
- `outputMode` controls whether the Dataset contains all records, only places and reviews, or places only.
- Supports concurrent place detail and review scraping in `search` mode.
- Pushes every result immediately to the Apify Dataset.
- Saves a run summary to Key-Value Store record `OUTPUT`.
- Supports Apify Proxy and a manual proxy URL fallback.
- Adds lead scoring for every full place record.
- Finds emails and social links by visiting each business website (on by default). Google Maps does not publish emails, so this is the only source. Records get top-level `email` (best address, own domain first), `emails`, and `socialLinks` fields.
- Supports pay-per-event monetization with charged Dataset writes for `place-listed`, `place-detail`, and `review`.

### Performance and Memory

**Concurrency**: The `maxConcurrency` input (default 3, max 100) controls how many places are scraped in parallel during search mode. Higher concurrency is faster but requires more memory.

**Memory allocation**: Memory is configured in the Apify Console when starting a run, not in the input form:

- **Minimum**: 256 MB (suitable for list mode or low concurrency)
- **Recommended**: 1024 MB (default, good for concurrency 3-10)
- **High concurrency**: 2048-4096 MB (for concurrency >20)

The Actor will use more memory as concurrency increases. If you see out-of-memory errors, reduce `maxConcurrency` or increase the memory allocation.

### Input

The Actor uses `.actor/input_schema.json`, so configuration is available as a typed Apify Console form.

Important fields:

- `mode`: `list`, `place`, or `search`.
- `outputMode`: `allRecords`, `placesAndReviews`, or `placesOnly`.
- `query`: business/category query for `list` and `search`, for example `coffee shops`. Used when `searchTerms` is empty.
- `searchTerms`: several queries in one run, for example `["restaurant", "cafe", "bar"]`. Each runs separately (case-insensitive duplicates are dropped) and places are deduplicated across terms.
- `gridSearch`: on by default. When one search returns fewer places than `maxPlaces`, the area is split into map tiles that are searched nearest-to-center first until `maxPlaces` is reached.
- `gridCellKm`: tile width, default `2`.
- `searchLocation`: free-form location, for example `Berlin, Germany`.
- `geolocationParameters`: grouped country, state/region, county, city, and postal code fields used when `searchLocation` is empty.
- `customGeolocation`: grouped geometry type, coordinates, radius, latitude, longitude, and coordinates JSON fields for anchoring the map area.
- `leadFilters`: optional minimum rating, minimum review count, website, phone, and category filters.
- `websiteEnrichmentDepth`: `contactPages` (default) or `homepageOnly`.
- `websiteTimeoutSeconds`: separate short timeout for website enrichment requests.
- `skipSocialWebsites`: skips enrichment of social profile URLs by default to reduce slow tails and 429 responses.
- `placeId`: required for `place`.
- `maxPlaces`: maximum places per search term (up to 10000).
- `maxReviews`: reviews per place. Use `0` to skip reviews.
- `maxTotalReviews`: global review cap across the run. Use `0` for no global cap.
- `maxConcurrency`: number of places to scrape in parallel in `search` mode.
- `language`: Google `hl` parameter.
- `gl`: Google country parameter.
- `proxyConfiguration`: Apify Proxy settings.
- `delaySeconds`: randomized delay between requests.
- `enableSqliteCheckpoint`: optional internal SQLite copy for deduplication/checkpointing.
- `enrichWebsites`: website enrichment for emails and social links. On by default.
- `enableBillingEvents`: legacy testing flag. Production monetization is detected from Apify pay-per-event pricing.

### Output

Dataset records are streamed as they are found:

- `recordType: "listing"` for search results.
- `recordType: "place"` for full place detail records with lead scoring.
- `recordType: "review"` for individual reviews.

Output modes:

- `allRecords`: saves listings, places, and reviews.
- `placesAndReviews`: saves full places and reviews, but skips listing rows.
- `placesOnly`: saves only full place records and skips review fetching.

The `OUTPUT` key-value store record contains:

- mode and query
- start and finish timestamps
- places listed
- places scraped
- reviews scraped
- duplicates skipped
- failed places
- website enrichments
- skipped website enrichments
- places with at least one email (`placesWithEmail`)
- `status: "aborted"` when the run was aborted (the record is still written)

### Getting more than ~200 results

One Google Maps text search stops at roughly 120-220 places, however large the city. Google also ignores the map position when the query names a place, so "restaurant in Berlin" always returns the same central results.

With `gridSearch` on, the Actor first runs the normal text search. If that returns fewer than `maxPlaces`, it searches the bare term (for example `restaurant`) tile by tile across the area and keeps only new places inside it:

- The area is the location's boundary from OpenStreetMap (city, district, postcode or state). With `customGeolocation.radiusKm` it is that circle's bounding box. With coordinates but no radius it is 5 km around the point.
- Tiles are searched nearest to the center first, so a small `maxPlaces` still gets the central places.
- Tiles are searched in parallel by `maxConcurrency` workers. With Apify Proxy each worker has its own proxy session and switches to a fresh one when Google keeps blocking it; failed tiles are retried once by another worker.
- A tile stops paging after two pages that add nothing new. The scan ends early after 40 tiles in a row find nothing new. Very large areas use bigger tiles automatically (about 400 tiles at most).
- With `customGeolocation` coordinates, the location text is not added to the search, so results stay around those coordinates.
- With pay-per-event pricing, a term never searches for more places than the remaining budget can pay for.
- The `OUTPUT` record lists, per term, how many places came from the text search and how many from the grid.

### Lead scoring

Each full place record includes:

```json
{
  "leadScore": {
    "score": 80,
    "tier": "hot",
    "reasons": ["Has phone", "Has website", "High rating"]
  }
}
```

Signals include phone, website presence or absence, rating, review volume, opening hours, photos, booking links, and commercial local categories.

### Ready-made tasks

`tasks/` holds seven saved configurations ([Apify tasks](https://docs.apify.com/platform/actors/running/tasks)) for common jobs:

| Task | What it runs |
|---|---|
| `smoke-test` | 20 cafes in Berlin, to check a new build works |
| `berlin-restaurant-leads` | 500 Berlin restaurants that have a website, with emails |
| `munich-trades-multi-term` | Electricians, plumbers and roofers in Munich, 300 each |
| `frankfurt-top-dentists` | Dentists rated 4.5+ with 50+ reviews and a phone number |
| `hamburg-cafes-list-only` | Up to 1,000 Hamburg cafes, listings only (fast, cheap) |
| `cologne-gyms-homepage-emails` | 300 gyms, emails from homepages only (faster enrichment) |
| `vienna-hotels-large` | 2,000 Vienna hotels, 2 GB memory, concurrency 20 |

To create them in your Apify account, or update them after editing the JSON files, run:

```bash
APIFY_TOKEN=<your token> python tasks/sync_tasks.py
```

The script uses the Actor `<your-username>~fast-google-map-scraper` by default. Pass a different Actor ID as the first argument if yours has another name. `python test_tasks.py` checks every task input against the input schema.

### Example input

```json
{
  "mode": "search",
  "outputMode": "placesAndReviews",
  "query": "dentists",
  "searchLocation": "Austin, United States",
  "customGeolocation": {
    "latitude": 30.2672,
    "longitude": -97.7431
  },
  "maxPlaces": 25,
  "maxReviews": 20,
  "maxTotalReviews": 200,
  "maxConcurrency": 3,
  "language": "en",
  "gl": "us",
  "delaySeconds": 1.5,
  "enrichWebsites": true,
  "websiteEnrichmentDepth": "contactPages",
  "websiteTimeoutSeconds": 8,
  "skipSocialWebsites": true,
  "leadFilters": {
    "minRating": 4.0,
    "mustHaveWebsite": true
  },
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": ["RESIDENTIAL"],
    "apifyProxyCountry": "US"
  }
}
```

### Cost notes

Use `list` mode when you only need names, ratings, categories, addresses, coordinates, and place IDs. It avoids detail and review endpoints.

Use `search` mode when you need complete lead records. Set `maxReviews` to `0` if reviews are not needed.

Use `placesOnly` when you want a lead list without review rows. It skips review fetching even if `maxReviews` is greater than `0`.

The default prefilled input is intentionally small: 5 places, `placesOnly`, and no reviews. This keeps Apify Store automated QA runs fast and gives new users a low-cost first test. Increase `maxPlaces`, switch output mode, and enable reviews for production runs.

`maxReviews` is a per-place limit. For example, `maxPlaces: 20` and `maxReviews: 50` can produce up to 1,000 reviews. Use `maxTotalReviews` when you want a global cap.

Increase `maxConcurrency` to process multiple places in parallel. Start with `2` or `3` when using proxies. Higher values can reduce runtime, but they also increase request pressure and blocking risk.

Website enrichment is on by default because it is the only way to get emails. Turn it off when you only need Maps data and want the fastest, cheapest run.

`websiteEnrichmentDepth: "contactPages"` (default) fetches the homepage and, if it has no email on the business's own domain, up to 4 same-site Impressum, contact, and about pages (falling back to `/impressum`, `/kontakt`, `/contact`). It stops as soon as an own-domain email is found, and each site gets a 25-second total budget. `homepageOnly` is faster but misses most emails, which usually sit on the contact or Impressum page. Extraction handles `mailto:` links, HTML-entity encoding, Cloudflare email protection, and `[at]`/`(dot)` obfuscation, and drops placeholder and tracking addresses such as `email@example.com` or Sentry IDs.

`websiteTimeoutSeconds` is independent from the main Google Maps request timeout. Keep it low, such as `6` to `8`, when you want predictable cost. `skipSocialWebsites` is enabled by default because social profile URLs rarely expose emails and often return 429 or slow responses through proxies.

Lead filters are applied after full place details are fetched:

```json
{
  "leadFilters": {
    "minRating": 4.2,
    "minReviewCount": 25,
    "mustHaveWebsite": true,
    "mustHavePhone": false,
    "categoryIncludes": "cafe"
  }
}
```

Filtered places are counted in the `OUTPUT.filteredPlaces` summary and skipped from Dataset output and review fetching.

### Location targeting

You can target by city and country:

```json
{
  "query": "coffee shops",
  "searchLocation": "Berlin, Germany",
  "gl": "de"
}
```

Or use structured geolocation fields:

```json
{
  "query": "coffee shops",
  "geoCountry": "Germany",
  "geolocationParameters": {
    "country": "Germany",
    "city": "Berlin",
    "postalCode": "10117"
  },
  "gl": "de"
}
```

You can also add custom latitude and longitude to anchor the map area:

```json
{
  "query": "coffee shops",
  "searchLocation": "Berlin, Germany",
  "customGeolocation": {
    "latitude": 52.520008,
    "longitude": 13.404954
  },
  "zoom": 13,
  "gl": "de"
}
```

For coordinate-heavy workflows, use `customCoordinates` as simple text:

```json
{
  "query": "coffee shops",
  "searchLocation": "Berlin, Germany",
  "customGeolocation": {
    "coordinates": "52.520008, 13.404954",
    "radiusKm": 5
  }
}
```

Or use `coordinatesJson` with a GeoJSON point:

```json
{
  "query": "coffee shops",
  "searchLocation": "Berlin, Germany",
  "customGeolocation": {
    "coordinatesJson": "{\"type\":\"Point\",\"coordinates\":[13.404954,52.520008]}"
  }
}
```

# Actor input Schema

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

list searches only and is cheapest; place scrapes one known place ID; search lists places, fetches details, and optionally reviews.

## `outputMode` (type: `string`):

Choose which records are saved to the Dataset. list mode always returns listing records.

## `query` (type: `string`):

Business/category query, for example 'dentists' or 'coffee shops'. Used when Search terms is empty.

## `searchTerms` (type: `array`):

Several queries to run in one go, for example 'restaurant', 'cafe', 'bar'. Each term is searched separately and places are deduplicated across terms. Overrides Search query when set.

## `searchLocation` (type: `string`):

Free-form location such as Berlin, Germany. Used together with the search query.

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

Known Google Maps place ID. Required for place mode.

## `maxPlaces` (type: `integer`):

Maximum places per search term. One Google search stops at about 120-220 results; Grid search finds more by splitting the area into map tiles.

## `gridSearch` (type: `boolean`):

When a search returns fewer places than Max places, split the area into map tiles and search each tile, nearest to the center first, until Max places is reached. The area is the location's boundary (from OpenStreetMap), or the Custom geolocation radius. Adds search requests only when needed.

## `gridCellKm` (type: `number`):

Width of one grid tile. Smaller tiles find more places in dense cities but need more requests. Very large areas use bigger tiles automatically (max 400 tiles).

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

Number of places to scrape in parallel in search mode. Higher values are faster but can increase blocking risk. Increase memory allocation if using high concurrency (>20).

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

Maximum reviews to fetch for each place. Set to 0 to skip reviews.

## `maxTotalReviews` (type: `integer`):

Global review cap across the whole run. Set to 0 for no global cap.

## `geolocationParameters` (type: `object`):

Optional structured location fields. These are used when the free-form location is empty.

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

Optional geometry and coordinate fields for anchoring the Google Maps search area.

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

Google Maps zoom level. Higher values narrow the search area.

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

Google hl language parameter.

## `gl` (type: `string`):

Google gl country parameter.

## `enrichWebsites` (type: `boolean`):

Visit each place's website to extract emails and social links. Google Maps itself never lists emails, so turn this off only if you do not need them. Works in search and place mode, not list mode.

## `websiteEnrichmentDepth` (type: `string`):

Homepage + contact pages (recommended) also checks impressum/contact/about pages and stops once an email on the business's own domain is found. Homepage only is faster but finds far fewer emails.

## `websiteTimeoutSeconds` (type: `integer`):

Timeout for each website enrichment request. Keep this lower than the Google Maps timeout to avoid slow websites increasing run cost.

## `skipSocialWebsites` (type: `boolean`):

Skip website enrichment when Google Maps lists a social profile such as Instagram, Facebook, LinkedIn, X, or Twitter. This is faster and avoids many 429 responses.

## `leadFilters` (type: `object`):

Optional filters applied after full place details are fetched. Filtered places are skipped from Dataset output and reviews.

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

Use Apify Proxy or custom proxies.

## `proxyUrl` (type: `string`):

Optional direct proxy URL used when Apify Proxy is disabled.

## `delaySeconds` (type: `number`):

Minimum delay between HTTP requests. Actual delay is randomized up to 2x this value.

## `timeoutSeconds` (type: `integer`):

HTTP request timeout in seconds.

## `enableSqliteCheckpoint` (type: `boolean`):

Keep an internal SQLite copy for deduplication/checkpointing. Dataset remains the primary output.

## `sqlitePath` (type: `string`):

SQLite checkpoint database path. Must be outside storage/key_value_stores/default and storage/datasets (SDK purges those directories at run start). Default: storage/gmaps_checkpoint.db

## `enableBillingEvents` (type: `boolean`):

Legacy testing flag. Production monetization is enabled automatically when pay-per-event pricing is configured in Apify Console. For local tests, use ACTOR_TEST_PAY_PER_EVENT=true.

## Actor input object example

```json
{
  "mode": "search",
  "outputMode": "placesOnly",
  "query": "coffee shops",
  "searchLocation": "Berlin, Germany",
  "maxPlaces": 5,
  "gridSearch": true,
  "gridCellKm": 2,
  "maxConcurrency": 3,
  "maxReviews": 0,
  "maxTotalReviews": 0,
  "zoom": 13,
  "language": "en",
  "gl": "de",
  "enrichWebsites": true,
  "websiteEnrichmentDepth": "contactPages",
  "websiteTimeoutSeconds": 8,
  "skipSocialWebsites": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "delaySeconds": 0.5,
  "timeoutSeconds": 30,
  "enableSqliteCheckpoint": false,
  "sqlitePath": "storage/gmaps_checkpoint.db",
  "enableBillingEvents": false
}
```

# Actor output Schema

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

Dataset items generated by the Actor. Depending on outputMode, this can include listing, place, and review records.

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

Key-value store OUTPUT record with counters, timing, effective query, location, filters, and run status.

# 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 = {
    "query": "coffee shops",
    "searchLocation": "Berlin, Germany",
    "enrichWebsites": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("solutionssmart/fast-google-map-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 = {
    "query": "coffee shops",
    "searchLocation": "Berlin, Germany",
    "enrichWebsites": True,
}

# Run the Actor and wait for it to finish
run = client.actor("solutionssmart/fast-google-map-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 '{
  "query": "coffee shops",
  "searchLocation": "Berlin, Germany",
  "enrichWebsites": true
}' |
apify call solutionssmart/fast-google-map-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,solutionssmart/fast-google-map-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/fyyQzl7Sfh8Zmqdfx/builds/aigPKeH5U35EfwdbB/openapi.json
