# Google Maps Scraper: Places, Leads, Emails (`snow_leo_data/google-maps-places-scraper-leads-emails`) Actor

Extract data from thousands of Google Maps locations and businesses: names, addresses, phones, websites, categories, opening hours, images, ratings and review counts, plus contact info - emails and social profiles. Export data, run via API, schedule and monitor runs, or integrate with other tools.

- **URL**: https://apify.com/snow\_leo\_data/google-maps-places-scraper-leads-emails.md
- **Developed by:** [Snow Leo Data](https://apify.com/snow_leo_data) (community)
- **Categories:** Lead generation, Marketing, Travel
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 places

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 Scraper: places, business leads and emails

Google Maps answers **about 150 places** for one search term in one map view
and then goes quiet — page seven comes back empty, whatever you ask. That is
the ceiling every Google Maps scraper hits, and it is the reason a "coffee in
Austin" export stops at a hundred and fifty rows while the city holds
thousands of cafes.

This Actor breaks that ceiling by splitting the map. It searches your area,
notices which parts came back full, cuts those parts into four and searches
each of them, and keeps going until the corners stop being crowded. Measured
on `coffee` in Austin, Texas on 12 September 2026: **160 places with no
splitting, 766 places at grid depth 2 — 4.79 times more**, in 42 seconds and
168 requests. The run is reproducible with `python3 tools/measure_ceiling.py`
and the raw output is kept in `tools/measurements.json`.

Everything is plain HTTPS from the Apify data centre. No proxies, no browser,
no API key, nothing to configure.

#### What you get for one place

One row per place, 56 columns. The identity block is `place_id`, `cid`, `fid`
and `kgmid` — four different ids because four different tools want four
different ones, and converting between them afterwards is a chore.

Then: `title`, `subtitle`, `description`, `category` and the full
`categories` list, the one-line `address` plus `street`, `city`,
`postal_code`, `state`, `country_code` and `neighborhood` split apart,
`plus_code`, `latitude`, `longitude`, `phone` as Google prints it and
`phone_e164` ready for a dialler or a CRM, `website` and its bare `domain`,
`rating`, `reviews_count` and `reviews_url`, `opening_hours` as a structured
week with numeric open/close times, the same week as one readable
`opening_hours_text` line, `additional_opening_hours` for separate lunch or
brunch schedules, `open_now`, `permanently_closed`, `timezone`, `owner_name`,
`attributes` (accessibility, service options, amenities, payment — grouped,
not a flat heap of strings), `image_urls` and `images_count`, `booking_links`,
`maps_url`, `search_page_url`, and the bookkeeping columns `search_term`,
`search_area`, `rank_in_search`, `language`, `source`, `change_type` and
`scraped_at`.

Switch on the website enrichment and seven more arrive: `emails`,
`email_count`, `social_profiles`, `social_platforms`, `social_count`,
`website_title`, `website_platform`, plus `website_status`,
`website_pages_crawled` and `website_error` so you can see what happened to
each site.

#### How full those columns actually are

Measured on 160 places across four cuts — `coffee` in Austin, `dentist` in
Manchester, `bakery` in Lyon and `hardware store` in Round Rock — and stored
in `tools/completeness.json`:

| Column | Search answer only | With the detail card (default) |
|---|---|---|
| title, address, coordinates, city, postcode, category | 100% | 100% |
| rating | 100% | 100% |
| opening hours | 97.5% | 97.5% |
| attributes | 91.2% | 91.2% |
| phone in E.164 | 89.4% | 89.4% |
| website | 86.2% | 86.2% |
| reviews count | 50.0% | **83.1%** |
| plus code | 0% | **100%** |

That gap is the whole reason the detail card exists. Google's search answer
is **not stable**: the same place, asked twice within a minute, comes back
once with the review count and once without. We measured the share of places
carrying a review count swinging between 37% and 100% over an hour on the
same query. So the Actor tops up incomplete rows from the place card and, if
the card also comes back thin, asks it one more time. That is `placeDetails:
auto`, and it is included in the price — the reference Actors in this niche
bill the same thing as a separate `place-details-scraped` event.

#### Telling the Actor where to look

Three ways, pick one:

Put what you are looking for into `searchTerms`, one term per line, then
pick the area.

**A place name.** Type `Austin, Texas`, `Bavaria`, `Portugal` or
`10115 Berlin` into **Location**. Google resolves it to the real
administrative boundary and the Actor searches inside that boundary — for
New York it returns 40.4766..40.9177 by -74.2588..-73.7002, which is the
city's actual outline, not a circle around Times Square.

A name Google does not recognise is **refused, not guessed**. That matters
more than it sounds: on a made-up name the source does not return an error,
it hands your own request back unchanged, and a naive scraper reads that as
"search the entire planet". Type `Zzqqxx Notaplace` here and you get an empty
run with the reason `location-not-understood` and no place request sent at
all.

**A point and a radius.** `latitude`, `longitude`, `radiusKm`. The rectangle
is built with the cosine of the latitude, so 10 km is 10 km in Oslo as well
as in Nairobi.

**An exact rectangle.** `boundingBox` as `{"south": …, "west": …, "north": …,
"east": …}`.

#### Grid depth, and what it costs

`gridDepth` is how many times an area may be cut in four:

| Depth | Areas at most | Measured on `coffee` in Austin |
|---|---|---|
| 0 | 1 | 160 places, 8 requests, 9 s |
| 1 | 4 | 505 places, 40 requests, 19 s |
| 2 | 16 | 766 places, 168 requests, 42 s |
| 3 | 64 | — |
| 4 | 256 | — |

Only the areas that hit the wall are cut. An area that returned thirty places
out of a possible hundred and fifty holds everything there is, and splitting
it would be four requests spent on nothing. Depth 2 is the default and is a
reasonable answer for a city.

#### Number of places means delivered places

`maxPlaces` counts rows that reach your dataset, not rows that were crawled.
This is not a detail. The usual way this goes wrong elsewhere: you ask for 30
places in the Construction category, the crawl stops after 30 places of any
category, the filter removes all 30, the run finishes green and the dataset is
empty. Here the crawl keeps going until your order is filled, and a per-term
quota (`maxPlacesPerTerm`) stops one popular term from eating the whole
budget before the next one starts.

#### Filters never empty your dataset by accident

Every filter follows one rule: **a missing field is not a mismatch.** If you
filter on a minimum rating and a place arrives without a rating, the Actor
fetches its detail card first and only then decides. If the card has no
rating either — a brand new business with no reviews — the row passes rather
than vanishing.

Available filters: `categoryContains`, `minRating`, `minReviews`,
`maxReviews`, `websiteFilter` (any / only with a website / only without),
`onlyWithPhone`, `onlyOpenNow`, `skipPermanentlyClosed`, `nameContains`,
`excludeContains`, `onlyCountries`, `onlyWithEmail` and
`onlyCorporateEmail`. The REPORT record at the end names which filter dropped
how many rows, so a thin result is never a mystery.

**Only places without a website** is the classic lead list: those are the
businesses that buy one. It pairs with `minReviews` to skip the dead
listings.

#### Emails and social profiles

Google Maps has no email field at all — it never did. Switch on
`enrichFromWebsite` and the Actor opens the business's own site, follows its
contact and imprint links, and reads out every address and social profile it
publishes: `mailto:` links, addresses written as text, `schema.org` JSON-LD,
and addresses that Cloudflare has obfuscated in the markup. Up to
`websitePagesPerPlace` pages per site, 3 by default.

A site that answers 403, or has quietly expired, does not remove the place
from your results. The row arrives with `website_status` and
`website_error` filled in, so the address can be marked dead in your own
database instead of silently disappearing from the export.

`onlyCorporateEmail` drops places whose only address is on Gmail, Yahoo,
Hotmail, GMX, Mail.ru and the like.

#### Watching places change over time

Switch on `onlyChanged` and the Actor remembers, between runs, what it has
already delivered. On the next run it returns only what is new or what moved:
a rating that went up, a review count that grew, a phone number that changed,
a website that appeared, an address that was corrected, opening hours that
were edited, or a place that closed for good. Every row carries
`change_type`: `NEW`, `UPDATED` or `UNCHANGED`.

The fingerprint deliberately ignores photos and booking links: Google
reshuffles those on every request and a monitor built on them would report
everything as changed, every day.

It also treats a **missing** field as "no news", not as a change. This is the
difference between a usable monitor and an expensive one. Measured in the
cloud before the fix: two runs over the same town forty seconds apart reported
89 of 92 bakeries as UPDATED, purely because Google had dropped their ratings
and trimmed their opening hours from the second answer. After the fix the same
three runs in a row returned 160 unchanged, then one genuinely new bakery, then
nothing - and said `nothing-changed` each quiet time. Opening hours only count
towards a change when a full seven-day week came back.

This is the mode the market actually runs in. The reference review scraper in
this niche shows 162 554 118 runs against 56 060 users — **2 900 runs per
user**, which is not one-off exporting, it is a schedule. Memory lives in a
**named** key-value store, so it survives between runs; the default store is
wiped at the start of every run and would have quietly turned monitor mode
into a plain re-export.

#### An empty result always says why

A green run with nothing in it is the worst thing a paid Actor can do. This
one never does it silently. The REPORT record carries `empty_reason`, one of
ten named values, and the log carries a sentence in plain English.

Six of them are decided **before a single byte is fetched**, because they are
decided by your input alone:

- `no-search-given` — no search term and no place id.
- `location-not-understood` — Google does not know that place name.
- `email-filter-without-enrichment` — you asked for places with an email but
  left the website enrichment off, and Maps itself carries no email, so every
  row would be dropped.
- `rating-filter-impossible` — a minimum rating above 5.0.
- `website-filter-contradicts-email` — places *without* a website and *with*
  an email at the same time; the email is read from the website.
- `charge-limit-reached` — the run's charge limit is already spent.

Four are only visible afterwards: `source-returned-nothing`, `filtered-out`,
`nothing-changed` (monitor mode working exactly as asked) and
`nothing-collected`.

#### Looking up places you already know

Paste ids into `placeIds` and skip the search entirely. Four formats are
accepted: a Google place id (`ChIJ…`), a numeric CID, an internal feature id
(`0x…:0x…`), and a plain Google Maps link with any of those in it. Anything
that is none of those is listed by name in `ignored_input` rather than
silently thrown away — a postcode typed into that box is not mistaken for a
CID.

#### Output shape

`compactOutput` cuts the row to 16 columns — name, where, how to contact,
how good — which is what an AI agent or a CSV import usually wants.
`dropEmptyFields` leaves out every column that has no value in that row.
Column order is fixed and does not depend on what arrived first, so a
scheduled export keeps the same CSV header from week to week.

#### Honest limitations

The reference Actor in this niche publishes 99 dataset fields; 58 of them
have an equivalent here. These do not, and were looked for in live payloads
on 12 September 2026 rather than assumed absent:

- **review text.** This Actor gives you the rating, the review count and the
  link to the reviews page. It does not pull individual reviews with author,
  date and text.
- **price band** (`$` / `$$` / `$$$`), **popular times**, **review
  distribution by star**, **review keyword tags**, **questions and answers**,
  **owner updates**, **"people also search for"**, **web results**.
- **hotel, restaurant and fuel verticals**: star ratings, hotel ads,
  reservation providers, online ordering widgets, menus, fuel prices.
- **decision-maker leads and email verification.** The reference sells both
  as $0.10-per-record add-ons backed by a third-party database. This Actor
  reads only what a business publishes on its own website.
- **photos beyond the first few**, and image authors.
- **scraping inside a mall or shopping centre**, and **exact-name matching**.
  `nameContains` here is a substring filter.
- The search area is a rectangle or a circle, not a GeoJSON polygon.

A full line-by-line table, including the billing comparison, is in
`COMPARISON.md`.

#### FAQ

**Do I need a Google API key?** No. Nothing is configured, nothing is billed
by Google, and there is no quota to run out of.

**Do I need proxies?** No. The source was probed from inside the Apify cloud
before a line of this Actor was written — twelve requests from six cities on
four continents, all 200, all with data. That check exists because it is
exactly where a scraper dies: a source that answers a laptop happily can
refuse every data-centre address.

**How is this different from the official Places API?** Places API charges
per call, caps a text search at 60 results, and needs a billing account. This
Actor has no key, returns the same core fields, and goes past 150 per search
by splitting the map.

**Why do some places have no phone or website?** Because the business never
published one. Measured on 160 live places: 89.4% had a phone and 86.2% had a
website. The remainder is real-world data, not a failure — and `websiteFilter:
without` turns exactly that gap into a lead list.

**Why is the review count sometimes missing?** Because Google leaves it out of
its own answer at random. The Actor asks the place card again when that
happens, which lifted coverage from 50.0% to 83.1% in our measurement. When
the count is genuinely absent everywhere, the field is `null` rather than a
guess.

**Can I search several terms at once?** Yes. Every term is searched across the
whole area. Use `maxPlacesPerTerm` if you want each term to get a fair share
of the budget.

**Does it work outside the United States?** Yes. The live test suite searches
Paris, Tokyo, Sydney and São Paulo on every run. Set `language` for the
language of names and categories, and `countryCode` for which Google front
end to ask.

**What happens if a run is interrupted?** Rows already pushed stay pushed, and
nothing is marked as delivered until it actually has been. The next run owes
you everything that did not arrive. This is covered by a test that cuts the
push in half on purpose.

**How do I run it every day?** Set a schedule, switch on `onlyChanged`, and
read `change_type`. Quiet runs mean nothing moved, and they say so with
`empty_reason: nothing-changed` instead of leaving you guessing.

**Is the data legal to collect?** It is public business information that
Google shows to anyone without a login. Treat personal data in it — an owner
name, a personal mobile — under the rules that apply to you.

# Actor input Schema

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

What to look for on the map, one term per line: "coffee", "dentist", "plumber near me". Every term is searched across the whole area you set below.

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

A city, region or country: "Austin, Texas", "Paris, France", "Bavaria". Google Maps resolves it to the real administrative boundary and the Actor searches inside that boundary. Leave empty to use coordinates or a bounding box instead.

## `latitude` (type: `string`):

Centre of a circular search area. Used only when Location is empty.

## `longitude` (type: `string`):

Centre of a circular search area. Used only when Location is empty.

## `radiusKm` (type: `integer`):

Radius around the point above. 0.2 to 500 km.

## `boundingBox` (type: `object`):

Exact rectangle: {"south": 30.1, "west": -97.9, "north": 30.5, "east": -97.6}. Wins over latitude/longitude when both are given.

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

How many places to deliver in total. Counted on DELIVERED rows, not on crawled ones: a filter can never leave you with an empty dataset because the crawl stopped too early. 0 means no limit.

## `maxPlacesPerTerm` (type: `integer`):

Stops one popular term from eating the whole budget. 0 means no per-term quota.

## `gridDepth` (type: `integer`):

Google answers at most about 150 places for one term in one map view. The Actor splits any area that hits that wall into four and searches each part. Depth 0 = no splitting, 1 = up to 4 parts, 2 = up to 16, 3 = up to 64, 4 = up to 256. Measured on coffee in Austin: 160 places at depth 0, 505 at depth 1, 766 at depth 2.

## `placeDetails` (type: `string`):

Google's search answer is not always complete: the same place comes back with opening hours and a rating in one request and without them in the next. "auto" fetches the detail card only for rows that are missing something or that a filter needs. It is included in the price, not billed separately.

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

Language of names, categories and opening hours, e.g. en, de, fr, es, ja.

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

Which Google front end to ask, e.g. us, de, fr. Affects ranking and address formatting, not the search area.

## `enrichFromWebsite` (type: `boolean`):

Google Maps never carries an email address. Switch this on and the Actor opens the business website, reads its contact pages and returns emails, social profiles and the site platform. Included in the price - the reference Actors in this niche bill it as a separate add-on event.

## `websitePagesPerPlace` (type: `integer`):

How deep to go on each business website. 1 is the home page only.

## `categoryContains` (type: `array`):

Keep only places whose Google category matches one of these words, e.g. "restaurant", "bar". Empty means every category.

## `minRating` (type: `string`):

Keep only places rated at least this, e.g. 4.5. Empty means any rating.

## `minReviews` (type: `integer`):

Keep only places with at least this many reviews. 0 means any.

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

Useful to find small or new businesses. 0 means any.

## `websiteFilter` (type: `string`):

"Without a website" is the classic lead list: those businesses are the ones who buy one.

## `onlyWithPhone` (type: `boolean`):

Drops places that publish no phone number. Measured on 160 live places: 89.4% had one.

## `onlyOpenNow` (type: `boolean`):

Keeps only places whose opening hours say they are open at the moment of the run.

## `skipPermanentlyClosed` (type: `boolean`):

Google keeps closed businesses on the map. On by default, because a closed place is not a lead.

## `nameContains` (type: `array`):

Keep only places whose name contains one of these words.

## `excludeContains` (type: `array`):

Drop places whose name, category, address or description contains one of these words.

## `onlyCountries` (type: `array`):

Two-letter country codes, e.g. us, de. Useful when a bounding box crosses a border.

## `onlyWithEmail` (type: `boolean`):

Needs the website enrichment above - Google Maps itself has no email field.

## `onlyCorporateEmail` (type: `boolean`):

Drops places whose only address is on gmail, yahoo, hotmail and the like.

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

Look up exact places instead of searching. Accepts Google place ids (ChIJ...), numeric CIDs, feature ids (0x...:0x...) and plain Google Maps links. Works together with a search.

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

Same as above, kept under the name other Actors use.

## `onlyChanged` (type: `boolean`):

Remembers what it delivered in earlier runs and returns only places that are new or whose rating, review count, phone, website, address or opening hours moved. Every row carries change\_type = NEW / UPDATED / UNCHANGED.

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

Keeps the change\_type column but delivers everything.

## `compactOutput` (type: `boolean`):

16 columns instead of 55 - name, where, contact, rating. Handy for AI agents and for CSV.

## `dropEmptyFields` (type: `boolean`):

Leaves out every field that has no value in this row.

## `concurrency` (type: `integer`):

How many requests run at the same time. Higher is faster and heavier on the source.

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

How long to wait for one request before giving up on it.

## Actor input object example

```json
{
  "searchTerms": [
    "coffee"
  ],
  "location": "Austin, Texas",
  "radiusKm": 5,
  "maxPlaces": 100,
  "maxPlacesPerTerm": 0,
  "gridDepth": 2,
  "placeDetails": "auto",
  "language": "en",
  "countryCode": "us",
  "enrichFromWebsite": false,
  "websitePagesPerPlace": 3,
  "minReviews": 0,
  "maxReviews": 0,
  "websiteFilter": "any",
  "onlyWithPhone": false,
  "onlyOpenNow": false,
  "skipPermanentlyClosed": true,
  "onlyWithEmail": false,
  "onlyCorporateEmail": false,
  "onlyChanged": false,
  "emitUnchanged": false,
  "compactOutput": false,
  "dropEmptyFields": false,
  "concurrency": 6,
  "requestTimeoutSecs": 30
}
```

# Actor output Schema

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

No description

## `overview` (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 = {
    "searchTerms": [
        "coffee"
    ],
    "location": "Austin, Texas"
};

// Run the Actor and wait for it to finish
const run = await client.actor("snow_leo_data/google-maps-places-scraper-leads-emails").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 = {
    "searchTerms": ["coffee"],
    "location": "Austin, Texas",
}

# Run the Actor and wait for it to finish
run = client.actor("snow_leo_data/google-maps-places-scraper-leads-emails").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 '{
  "searchTerms": [
    "coffee"
  ],
  "location": "Austin, Texas"
}' |
apify call snow_leo_data/google-maps-places-scraper-leads-emails --silent --output-dataset

```

## MCP server setup

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

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/ydWpIhvn33Z97UmZm/builds/UbOgk7h2rGs0H1wsI/openapi.json
