# Google Maps Leads Scraper — unlimited volume, no 120 cap (`memo23/google-maps-leads-scraper`) Actor

Google caps every Maps search at ~120 results. This one tiles the search area and subdivides until it has them all - 783 businesses where a single search returns 140. Phone, website, rating, category, full address, coordinates per row. Optional emails and web-platform detection.

- **URL**: https://apify.com/memo23/google-maps-leads-scraper.md
- **Developed by:** [Muhamed Didovic](https://apify.com/memo23) (community)
- **Categories:** Lead generation, Automation, AI
- **Stats:** 14 total users, 14 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 business scrapeds

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/platform/actors/running/actors-in-store#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 Leads Scraper

Search Google Maps by keyword and location, and export business leads with phone,
website, rating, category, full address, and coordinates.

### The thing that makes this different

**Google caps every Maps search at roughly 120 results.** Search "restaurant" in
Manhattan and you get ~120 back, whether 120 exist or 12,000 do. Every Google Maps
scraper on the market inherits that ceiling, because it is Google's, not theirs.

Turn on **Exhaustive coverage** and this actor works around it. The search area is
covered with tiles; any tile that comes back at the ceiling is assumed truncated,
split into four smaller tiles, and re-searched. Repeat until every tile returns
comfortably under the cap.

Measured on a ~6×8 km slice of Manhattan searching "restaurant":

| | Places found | Requests |
|---|---|---|
| Single search (what other Maps scrapers return) | 140 | 10 |
| Exhaustive coverage, 10-tile budget | **783** | 83 |

**706 of those 783 are unreachable by any single search**, no matter how you page it.
That run was still budget-limited with 18 tiles queued, so 783 is a floor, not a
ceiling. Cost worked out to 0.11 requests per business.

The same mechanism scales up: point it at a state instead of a neighbourhood and it
tiles the state.

### Cost control

Tiling costs requests, and cost scales with **tiles searched, not places found** — a
dense metro can subdivide for a long time. Two controls bound it:

- **Tile budget per search** — a hard ceiling on tiles per search term × location.
  When it runs out, the run logs how much of the area went uncovered rather than
  silently returning partial data.
- **Starting tile radius** — larger tiles cost fewer requests but subdivide more.
  8 km suits a city; 2–3 km suits a dense downtown.

Leave exhaustive coverage off and you get one search per term/location, at the usual
\~120 ceiling and the usual cost.

### What you get per business

Every search result already carries the full lead record, so no per-place follow-up
request is needed — roughly **one HTTP request per 20 leads**:

**Identity and contact** — `name` · `phone` · `phoneRaw` · `website` ·
`websiteDomain` · `address` + split `streetAddress`/`city`/`state`/`postalCode`/
`countryCode` · `latitude`/`longitude` · `neighborhood` · `timezone`

**Reputation and profile** — `rating` · `category` + all `categories` ·
`description` (Google's editorial blurb) · `highlights` ("LGBTQ+ friendly",
"Identifies as Latino-owned", …) · `additionalInfo` (grouped attributes such as
Accessibility) · `owner` · `thumbnail`

**Operations** — `businessHours` · `currentStatus` ("Open · Closes 11 PM") ·
`bookingLinks` (OpenTable, Resy, SevenRooms, order-online) · `hotel` for lodging
(star rating, Google's suggested check-in/check-out dates, and partner nightly rates)

**Keys** — `featureId` · `cid` · `placeIdChIJ` · `knowledgeId` · `googleMapsUrl` ·
`searchKeyword` · `searchLocation`

#### Two honest limitations

**No review count, price range, or popular times.** Google's search payload does not
contain them. This was verified rather than assumed: review counts were checked
against exhaustively-paginated totals for real businesses and matched nothing in the
response (the closest values were image dimensions), and no price-band string or
hourly-busyness array exists anywhere in the payload. Shipping a plausible-looking
wrong number would be worse than omitting the field.

**Opening hours cover the upcoming day, not the full week.** That is all Google puts
in this response.

Optionally, **Find emails on each business website** visits each business's own site
and harvests public contact emails and phone numbers.

### Segment by tech stack, rank by quality

**Detect web platform** fetches each business site once and reports two things
separately, because they answer different questions:

- `webHost` — what the site is **built** with (WordPress, Shopify, Wix, Squarespace,
  Webflow, Ghost, Drupal, Magento, BigCommerce, …)
- `webHosting` — where it **runs** (WP Engine, Kinsta, Vercel, Netlify) or which CDN
  fronts it (Cloudflare, Fastly, Akamai, CloudFront)

A Shopify store behind Cloudflare reports both, so a filter for either one matches it.
Set **Only keep these web platforms** and non-matching leads are dropped before
billing — useful if you're an agency that only sells to one stack.

**Score each lead** adds a 0–100 `leadScore` plus `leadScoreSignals` explaining it.
It is computed entirely from fields already on the row, so it costs no extra requests
and can't fail. The heaviest weight is on an email at the company's own domain
(`hello@theirsite.com` beats `theirshop99@gmail.com`), then phone, socials, rating,
and a complete address. Every weight is overridable.

Turn on email enrichment alongside it. The email signals are 39 of the 100 points, so
scoring without them compresses every lead into a narrow band — on a real run, scores
spread 33–85 with emails on versus 33–46 with them off. The actor logs a warning if
you enable scoring without enrichment.

### Filters (free — filtered rows are never charged)

Minimum rating · has-a-website · web platform · place IDs to skip ·
search circle (lat/lng/radius) · search polygon (GeoJSON ring).

Geographic filters are applied to coordinates the search already returned, so places
outside your circle or polygon are dropped before any billable work. There is no
minimum-review-count filter, because the review count itself is not in the payload
(see the limitations above).

### Input example

```json
{
  "searchTerms": ["dentist", "orthodontist"],
  "locations": ["Austin, TX"],
  "exhaustiveCoverage": true,
  "tileRadiusKm": 8,
  "maxTilesPerSearch": 40,
  "minRating": 4,
  "hasWebsite": true,
  "maxResults": 5000
}
```

Every search term is crossed with every location, so the example above runs four
searches.

### How locations are resolved

Location names are resolved to a centre **and a boundary** via OpenStreetMap's
Nominatim service, which is what makes state- and country-scale tiling possible
("Texas" resolves to a ~1,200 km box). Resolutions are cached in the run's key-value
store. If a name cannot be resolved, the actor falls back to locating it through
Google itself and covers the fallback radius instead.

### Notes

- Google blocks datacenter traffic quickly; **residential proxies are strongly
  recommended** and are the default.
- No browser is used — this talks to Google's own JSON endpoints over HTTP, which is
  why it is fast and cheap to run.
- Non-paying Apify users are capped at 100 places per run.

### 🤖 For AI Agents & LLM Apps

Compact reference for AI agents calling this actor via the [Apify MCP server](https://mcp.apify.com) or the Apify API (actor: `memo23/google-maps-leads-scraper`).

**Purpose:** Search Google Maps by keyword × location and export business leads (phone, website, rating, category, full address, coordinates); exhaustive tiling works past Google's ~120-result-per-search ceiling.

**Minimal input:**

```json
{
  "searchTerms": ["dentist"],
  "locations": ["Austin, TX"],
  "maxResults": 20
}
```

To go past Google's ~120-result cap, add `"exhaustiveCoverage": true` and bound it with `"maxTilesPerSearch": 40`.

**Output:** one row per business — `name`, `phone`, `phoneRaw`, `website`, `websiteDomain`, `address` + `streetAddress`, `city`, `state`, `postalCode`, `countryCode`, `latitude`, `longitude`, `neighborhood`, `timezone`, `rating`, `category`, `categories`, `description`, `highlights`, `additionalInfo`, `owner`, `thumbnail`, `businessHours`, `currentStatus`, `bookingLinks`, `hotel`, `featureId`, `cid`, `placeIdChIJ`, `knowledgeId`, `googleMapsUrl`, `searchKeyword`, `searchLocation`. Optional enrichment adds emails, `webHost`/`webHosting`, and `leadScore` + `leadScoreSignals`.

**Behaviors an agent should know:**

- Every `searchTerms` entry is crossed with every `locations` entry; set `maxResults` (whole run) and `maxPlacesPerSearch` to cap billing.
- `exhaustiveCoverage` splits the area into tiles and subdivides truncated ones; cost scales with **tiles searched**, not places found — bound it with `maxTilesPerSearch` and tune `tileRadiusKm`.
- Filters are free (filtered rows are never charged): `minRating`, `hasWebsite`, `webHostFilter`, `placeIdsToSkip`, and geographic `searchCircle`/`searchPolygon` (polygon takes precedence, both replace `locations`).
- Enrichment costs one request per business with a website: `includeWebsiteEmails` (emails) and `detectWebHost` (platform/hosting). `enableLeadScoring` is free but compresses without emails — enable enrichment alongside it. Use `placeIdsToSkip` to dedup across runs.
- Residential proxy is the default and strongly recommended; non-paying Apify users are capped at 100 places per run.
- Not in Google's payload: review count, price range, popular times; opening hours cover only the upcoming day.
- Pay-per-event billing — see the Pricing tab on the actor page.

### ⚠️ Disclaimer

This Actor is an independent tool and is not affiliated with, endorsed by, or sponsored by Google LLC or any of its subsidiaries. All trademarks mentioned are the property of their respective owners.

The scraper accesses only publicly available Google Maps business listing data — the same information any visitor sees in a browser — through Google's public endpoints; it does not log in, bypass authentication, or access private or paid features. Business contact details are personal data. You are responsible for ensuring your use complies with Google's Terms of Service, applicable data-protection law (GDPR, CCPA, etc.), marketing and anti-spam rules, and any contractual obligations of your own organization.

### SEO Keywords

google maps scraper, scrape google maps, google maps leads scraper, google maps business scraper, google places scraper, local business leads, b2b lead generation, business contact scraper, email finder, phone number scraper, lead scoring, google maps data extractor, google maps API, local seo data, business directory scraper, web tech stack detection, WordPress site finder, Shopify store finder, agency lead-gen, restaurant data scraper, Apify google maps

# Actor input Schema

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

Keywords to search on Google Maps, one per line. Every term is searched in every location below.

## `locations` (type: `array`):

Cities, regions, or countries to search, one per line. Each is paired with every search term. A city name alone works ('Austin'); add state or country for precision ('Austin, TX').

## `exhaustiveCoverage` (type: `boolean`):

Costs more requests than a single search — bound it with the tile budget below.

## `tileRadiusKm` (type: `integer`):

Size of the initial search tiles. Smaller tiles cost more requests but need less subdividing. 8 km suits a city; drop to 2-3 km for a dense downtown.

## `maxTilesPerSearch` (type: `integer`):

Hard cap on tiles searched for each search term and location pair. This is the run's main cost control: a dense metro can otherwise subdivide for a long time. When the budget runs out, the run logs how much of the area it covered.

## `defaultRadiusKm` (type: `integer`):

Area radius used when a location name cannot be resolved to a boundary.

## `searchCircle` (type: `object`):

Limit the search to a circular area: { "lat": <number>, "lng": <number>, "radiusMiles": <number> }. Replaces the Locations field when set. Places outside the circle are dropped without charge.

## `searchPolygon` (type: `array`):

Limit the search to an arbitrary shape: an array of \[lng, lat] pairs (GeoJSON order; first and last point may match). Takes precedence over the search circle. Places outside the polygon are dropped without charge.

## `minRating` (type: `integer`):

Skip places rated below this.

## `hasWebsite` (type: `boolean`):

Skip listings that have no website.

## `placeIdsToSkip` (type: `array`):

Feature IDs or CIDs you already collected. Skipped without charge. Paste many at once: one per line, or comma-separated.

## `includeWebsiteEmails` (type: `boolean`):

Visit each business's own website and harvest public contact emails and phone numbers. Adds one request per business that has a website.

## `maxEmailsPerPlace` (type: `integer`):

Cap on emails kept per business, best-ranked first.

## `detectWebHost` (type: `boolean`):

Fetch each business website once and report what it is built on (platform) and what it runs behind (hosting/CDN). Useful for B2B segmentation by tech stack.

## `webHostFilter` (type: `array`):

Keep only businesses whose site runs one of these. Turns detection on automatically. Non-matching leads are dropped before billing. Useful if you are an agency that only works with one stack.

## `enableLeadScoring` (type: `boolean`):

Add a 0-100 score computed from signals already on the row: email on the company's own domain, business vs free mailbox, phone, website, rating, socials, description, images, attributes, complete address. Free, no extra requests.

## `leadScoreWeights` (type: `object`):

Override any subset of the defaults: emailDomainMatch (25), phonePresent (12), socialAny (10), emailPresent (8), websitePresent (8), ratingAtLeast4 (8), addressComplete (8), emailProviderBusiness (6), descriptionPresent (5), imagePresent (5), additionalInfoPopulated (5). Keys you omit keep their default.

## `maxPlacesPerSearch` (type: `integer`):

Cap on places returned for each search term and location pair.

## `maxResults` (type: `integer`):

Cap across the entire run.

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

Google Maps interface language, which affects the text returned.

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

Two-letter country code biasing the search session.

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

Parallel website-enrichment lookups.

## `proxy` (type: `object`):

Google Maps blocks datacenter traffic quickly. Residential proxies are strongly recommended.

## Actor input object example

```json
{
  "searchTerms": [
    "dentist"
  ],
  "locations": [
    "Austin, TX"
  ],
  "exhaustiveCoverage": false,
  "tileRadiusKm": 8,
  "maxTilesPerSearch": 40,
  "defaultRadiusKm": 25,
  "hasWebsite": false,
  "includeWebsiteEmails": false,
  "maxEmailsPerPlace": 3,
  "detectWebHost": false,
  "enableLeadScoring": false,
  "maxPlacesPerSearch": 200,
  "maxResults": 1000,
  "language": "en",
  "country": "us",
  "maxConcurrency": 8,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}
```

# 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": [
        "dentist"
    ],
    "locations": [
        "Austin, TX"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("memo23/google-maps-leads-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 = {
    "searchTerms": ["dentist"],
    "locations": ["Austin, TX"],
}

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,memo23/google-maps-leads-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/FjgJpxUMhXaAhchUG/builds/Fw8RgvKYHbvP0R9BA/openapi.json
