# Google Maps Scraper Turbo: Business Leads, Phones & Emails (`meskaoui.azeddine/google-maps-turbo-scraper`) Actor

Scrape Google Maps business leads for $0.40 per 1,000 places: name, phone, website, rating, review count, opening hours and coordinates, plus emails and social links on request. No browser, so 1,000+ places in minutes, past Google's 120-result limit. CSV, Excel, JSON, API.

- **URL**: https://apify.com/meskaoui.azeddine/google-maps-turbo-scraper.md
- **Developed by:** [Azeddine MESKAOUI](https://apify.com/meskaoui.azeddine) (community)
- **Categories:** Lead generation, Automation, SEO tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

## Google Maps Turbo Scraper — every business in an area, without a browser

Extract Google Maps businesses — name, address, phone, website, rating, reviews count, opening hours, coordinates — as a clean spreadsheet. No browser is launched, so runs finish in a fraction of the compute a browser-based Google Maps scraper needs.

**Measured, not promised:** 1,312 dentists in Casablanca in one run of 3 min 26 s on Apify, default settings, 510 requests, none failed (October 2026). A plain map search for the same term stops at about 120.

### Pricing

**$0.40 per 1,000 places** ($0.0004 per place delivered), plus $0.00005 per run start. You pay for results, not for compute time or proxies.

| You extract | You pay |
|---|---|
| 1,000 places | $0.40 |
| 10,000 places | $4.00 |
| 100,000 places | $40.00 |

Contact enrichment (emails and social profiles from the business website) is optional and costs $0.002 per place, charged only when something was found. A run that fails or returns nothing costs you the start event only.

Two things set it apart:

- **Adaptive area tiling.** Google returns at most ~120 results for any single map query, no matter how far you scroll. This Actor detects a saturated tile and quarters it, recursively, until the whole area is covered. A dense downtown gets subdivided; empty countryside is covered in one pass.
- **Drift-resistant parsing.** Google's payload is a deeply nested untyped array. Most scrapers hardcode index paths and silently start emitting empty phones and websites the day Google shifts an index. Here every field falls back to a shape-based scan (a place ID *looks* like `ChIJ…`, a rating is a number between 1 and 5) and the run writes a `FIELD_HEALTH_REPORT` telling you exactly which fields drifted.

### What it extracts

| Field | Description |
|---|---|
| `title` | Business name |
| `categoryName`, `categories` | Primary and full Google category list |
| `address`, `street`, `city`, `postalCode`, `countryCode` | Full and split address |
| `latitude`, `longitude`, `plusCode` | Location |
| `phone` | Phone number as shown on the listing |
| `phoneUnformatted` | Same number in international form, digits only (`+212612345678`) |
| `website`, `domain` | Business website and its bare domain |
| `totalScore`, `reviewsCount` | Star rating and number of reviews |
| `priceLevel` | `$` … `$$$$` when Google shows it |
| `openingHours` | Array of `{ day, hours[] }` |
| `permanentlyClosed`, `temporarilyClosed` | Business status flags |
| `imageUrl` | Main listing photo |
| `placeId`, `ftid`, `cid`, `url` | Stable identifiers and the canonical Maps link |
| `emails`, `socials` | With contact enrichment on: emails and social profiles from the business website |
| `searchString`, `scrapedAt` | Provenance of the row |

### How to use it

1. Type a **search term** — what you would type into Maps, e.g. `dentist`.
2. Type a **location** — simplest form first, e.g. `Casablanca, Morocco`. A whole region works; the Actor tiles it for you.
3. Set **places per search term**, click **Start**, then export as CSV, Excel, JSON or pull it from the API.

Turn on **Extract emails & socials** to convert the result into a lead list.

### Example input

```json
{
    "searchStringsArray": ["dentist"],
    "locationQuery": "Casablanca, Morocco",
    "countryCode": "ma",
    "maxCrawledPlacesPerSearch": 200,
    "language": "fr",
    "tilingMode": "auto",
    "extractContactsFromWebsite": false,
    "proxyConfiguration": { "useApifyProxy": true }
}
```

### Example output

```json
{
    "placeId": "ChIJreV9aqYWdkgROM_boL6YbwA",
    "ftid": "0xda7cd2f0b0b0b0b:0x1234abcd5678ef90",
    "cid": "1311768467284833680",
    "title": "Cabinet Dentaire Anfa",
    "categoryName": "Dentist",
    "categories": ["Dentist", "Dental clinic"],
    "address": "12 Rue Ahmed Chaouki, Casablanca 20250, Morocco",
    "street": "12 Rue Ahmed Chaouki",
    "city": "Casablanca",
    "postalCode": "20250",
    "countryCode": "MA",
    "latitude": 33.5731,
    "longitude": -7.5898,
    "phone": "05 22 12 34 56",
    "phoneUnformatted": "+212522123456",
    "website": "https://cabinet-dentaire-casa.ma/",
    "domain": "cabinet-dentaire-casa.ma",
    "totalScore": 4.6,
    "reviewsCount": 238,
    "priceLevel": "$$",
    "permanentlyClosed": false,
    "openingHours": [{ "day": "Monday", "hours": ["9:00 AM–6:00 PM"] }],
    "url": "https://www.google.com/maps?cid=1311768467284833680",
    "emails": ["contact@cabinet-dentaire-casa.ma"],
    "socials": { "instagram": ["https://instagram.com/cabinetdentairecasa"] },
    "searchString": "dentist",
    "scrapedAt": "2026-08-19T10:12:44.001Z"
}
```

### Use cases

- **Local lead generation** — build a prospect list with phone, website and email for one trade in one city.
- **Web-design and SEO prospecting** — set the website filter to *only without a website* and get businesses that need one.
- **Market and competitor mapping** — count and rank every competitor in a catchment area by rating and review volume.
- **Franchise and retail site selection** — export coordinates and density for an area before signing a lease.
- **Data enrichment** — feed place IDs or Maps URLs you already hold and get back fresh, structured records.

### Choosing a tiling strategy

| Mode | Requests | Coverage | Use when |
|---|---|---|---|
| `off` | 1 per term | First ~120 places | You just want the top results, fast |
| `auto` (default) | Splits only saturated tiles | High | Almost always |
| `aggressive` | Splits earlier, up to depth 5 | Highest | Dense city centres, exhaustive census |

### FAQ

**How do I export the results to Excel?**
Open the run's **Storage → Dataset** tab and pick *Export → Excel* (or CSV / JSON). The API endpoint on the same tab returns the same data for automated pipelines.

**Why do I get fewer places than exist in the city?**
Google caps a single query at ~120 results. Raise `maxTileDepth`, lower `saturationThreshold`, or switch `tilingMode` to `aggressive`. Also add more distinct search terms — `dentist`, `orthodontist`, `dental clinic` surface partly different sets.

**Do I need a proxy?**
Keep the default (Apify Proxy, automatic group). In our tests datacenter IPs returned 300 places in 13 seconds and 1,312 places in under 4 minutes, with no failed request. Google does slow some datacenter IPs down; the Actor notices within seconds, drops that IP and continues on another one, so you do not pay for the waiting. Switch to the `RESIDENTIAL` group only if a large run reports many dropped sessions in `RUN_HEALTH`.

**Some places have no email. Why?**
Emails come from the business website, not from Google. A business with no website, or one that hides its address behind a contact form or an image, yields none. Expect roughly 30–50% coverage on a typical local sector.

**Is scraping Google Maps legal?**
Business names, addresses and phone numbers are public business data, and this Actor collects no personal data by default. Reviewer profiles and named individual contacts are personal data under the GDPR and similar laws — if you process them, make sure you have a lawful basis. This is not legal advice.

**How much does 10,000 places cost?**
$4.00: 10,000 places at $0.0004 each, plus $0.00005 for the run start. With contact enrichment on, add $0.002 for each place where an email or social profile was found.

**Can I reuse my existing Google Maps Scraper task JSON?**
Mostly yes — `searchStringsArray`, `locationQuery`, `maxCrawledPlacesPerSearch`, `language`, `countryCode`, `city`, `state`, `postalCode`, `startUrls`, `placeIds`, `skipClosedPlaces`, `placeMinimumStars`, `website`, `searchMatching` and `proxyConfiguration` keep the same names and meanings. Unknown extra keys are ignored.

### Known limits — read before you buy

- **Reviews and photos are not extracted.** This Actor is deliberately a *places* scraper. Review text needs a separate paginated endpoint and would multiply run cost; use a dedicated reviews Actor for that.
- **No browser means no rendering fallback.** If Google serves a consent interstitial or a CAPTCHA to your proxy IP, the request fails and is retried on another IP rather than being solved.
- **Popular times, Q\&A, menus, hotel and reservation fields are out of scope.**
- **Google changes its internal format a few times a year.** The Actor is test-run every day against live data. When Google does break it, the run **fails with a clear message instead of returning an empty dataset**, so you never mistake a broken run for "no results". If you see "Google ignored the requested map area", it is on us: open an issue and it gets fixed.
- **Review counts depend on Google's goodwill.** The Actor obtains an anonymous session before searching, which is what makes Google return them; `RUN_HEALTH.countRetries` tells you if a run had to fight for them.
- **`plusCode` is usually empty**: Google no longer includes it in list results.
- **Geocoding uses OpenStreetMap.** Its idea of a city boundary can be tighter than Google's — for metro areas, prefer `customGeolocation` with a circle.

# Actor input Schema

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

What you would type into the Google Maps search bar, e.g. <b>dentist</b> or <b>coffee shop</b>. One term per line. Distinct terms cover more ground than near-synonyms.

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

Free text, simplest form first: <b>City, Country</b>. A whole country or region works too — the Actor splits it into tiles internally. Resolved via OpenStreetMap; check your area on <a href='https://nominatim.openstreetmap.org/ui/search.html'>Nominatim</a> if results look off.

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

Hard cap per search term. Leave empty or 0 to extract everything the area yields — combine with <b>Total places cap</b> to protect your budget.

## `maxTotalPlaces` (type: `integer`):

Absolute ceiling for the whole run. 0 means no ceiling. This is your budget guardrail.

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

Language of the returned place details.

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

Two-letter country code used to bias Google's results and to constrain geocoding, e.g. <b>ma</b>, <b>fr</b>, <b>us</b>.

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

City name alone — do not include state or country here.

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

State or region, mostly useful for US and large federal countries.

## `county` (type: `string`):

County, regional district or département depending on the country.

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

One postal code at a time. Combine with Country, never with City.

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

GeoJSON-style area, coordinate order <b>\[longitude, latitude]</b>. Supports <code>Polygon</code>, <code>MultiPolygon</code> and <code>Circle</code> (with <code>radiusKm</code>). Overrides all location fields above.

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

Google Maps search or place URLs. Place URLs (including <code>?cid=</code> and <code>place_id:</code> forms) are scraped directly; search URLs are treated as a search term.

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

Google Place IDs, e.g. <code>ChIJreV9aqYWdkgROM_boL6YbwA</code>. One per line.

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

Keep only places whose Google category contains one of these words (case-insensitive substring match). Leave empty to keep everything.

## `searchMatching` (type: `string`):

Restrict results by how the place name relates to your search term.

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

Keep only places rated at or above this. Places with no rating are dropped.

## `website` (type: `string`):

Keep only places that have a website, or only those that do not (useful for web-design prospecting).

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

Drop places marked permanently or temporarily closed.

## `extractContactsFromWebsite` (type: `boolean`):

Visits each business website and pulls emails, social profiles and tel: numbers. Slower and billed as a separate event, but this is what turns a place list into a lead list.

## `contactPagesPerSite` (type: `integer`):

Beyond the homepage, how many contact/about/legal pages to open per site. 0 = homepage only.

## `tilingMode` (type: `string`):

Google returns at most ~120 places per query. <b>Auto</b> splits a tile into four only when it saturates. <b>Aggressive</b> splits earlier and deeper — best coverage, more requests. <b>Off</b> sends one query per term (fastest, least complete).

## `maxTileDepth` (type: `integer`):

How many times a tile may be quartered. Each level multiplies request count by up to 4 — raise it only for dense city centres.

## `saturationThreshold` (type: `integer`):

A tile returning at least this many places is considered saturated and gets split.

## `minTileMeters` (type: `integer`):

Stop splitting once a tile's longest side falls below this. Prevents pointless subdivision of a single street.

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

Parallel HTTP requests. 20 is a good default with Apify Proxy; lower it if you see blocks.

## `dedupeAcrossSearches` (type: `boolean`):

A place found by two different terms is emitted once. Turn off if you need one row per (term, place).

## `calibrationMode` (type: `boolean`):

Saves up to 5 raw Google payloads to the key-value store as <code>CALIBRATION_SAMPLES</code>. Only useful for debugging with the developer.

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

Keep Apify Proxy on. The automatic group works: IPs that Google slows down are dropped within seconds and replaced. Residential is only worth its price on very large runs.

## Actor input object example

```json
{
  "searchStringsArray": [
    "dentist"
  ],
  "locationQuery": "Casablanca, Morocco",
  "maxCrawledPlacesPerSearch": 20,
  "maxTotalPlaces": 0,
  "language": "en",
  "customGeolocation": {
    "type": "Circle",
    "coordinates": [
      -7.5898,
      33.5731
    ],
    "radiusKm": 10
  },
  "searchMatching": "all",
  "placeMinimumStars": "",
  "website": "allPlaces",
  "skipClosedPlaces": false,
  "extractContactsFromWebsite": false,
  "contactPagesPerSite": 2,
  "tilingMode": "auto",
  "maxTileDepth": 3,
  "saturationThreshold": 55,
  "minTileMeters": 400,
  "maxConcurrency": 20,
  "dedupeAcrossSearches": true,
  "calibrationMode": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `places` (type: `string`):

All scraped places: name, category, address, phone, website, rating, review count, opening hours, coordinates and Google identifiers.

## `runHealth` (type: `string`):

Counters for this run: search responses, retries, failed requests and proxy sessions dropped for being too slow.

## `fieldHealth` (type: `string`):

Which fields were read from their expected position and which had to be recovered heuristically.

# 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": [
        "dentist"
    ],
    "locationQuery": "Casablanca, Morocco",
    "maxCrawledPlacesPerSearch": 20,
    "customGeolocation": {
        "type": "Circle",
        "coordinates": [
            -7.5898,
            33.5731
        ],
        "radiusKm": 10
    },
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("meskaoui.azeddine/google-maps-turbo-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": ["dentist"],
    "locationQuery": "Casablanca, Morocco",
    "maxCrawledPlacesPerSearch": 20,
    "customGeolocation": {
        "type": "Circle",
        "coordinates": [
            -7.5898,
            33.5731,
        ],
        "radiusKm": 10,
    },
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("meskaoui.azeddine/google-maps-turbo-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": [
    "dentist"
  ],
  "locationQuery": "Casablanca, Morocco",
  "maxCrawledPlacesPerSearch": 20,
  "customGeolocation": {
    "type": "Circle",
    "coordinates": [
      -7.5898,
      33.5731
    ],
    "radiusKm": 10
  },
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call meskaoui.azeddine/google-maps-turbo-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,meskaoui.azeddine/google-maps-turbo-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/4wgHjeSZyTVz0isyG/builds/cpCQA5gpr0zWpPkqY/openapi.json
