# Google Maps Business Scraper — Country-Scale (`oswaldocarabano/google-maps-scraper`) Actor

Extracts every business Google Maps publishes inside a bounding box, by recursive geographic tiling. No login, no browser, no cookies.

- **URL**: https://apify.com/oswaldocarabano/google-maps-scraper.md
- **Developed by:** [Oswaldo Carabano](https://apify.com/oswaldocarabano) (community)
- **Categories:** Lead generation, Business, Developer tools
- **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.
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 — Country-Scale

Extracts **every business Google Maps publishes inside an area** — not the first 200.
No API key, no login, no cookies, no browser.

Google caps any single Maps query at about **200 results**. So "all the restaurants in
Miami" is not one big query: it is a tiling problem. This Actor splits the map into
tiles, and splits again wherever a tile comes back full, until every tile returns under
the cap. Then it tells you honestly whether it finished.

***

### Quick start

1. Pick a **country** and one or more **search terms** in that country's language
   (English in the US and Canada, Spanish elsewhere).
2. Optionally give a **bounding box** to scrape a single city instead of a whole country.
3. Set **Maximum places** if you want a hard cap on what the run costs.

```json
{
  "country": "US",
  "categories": ["restaurant"],
  "bbox": { "lat_min": 25.70, "lng_min": -80.32, "lat_max": 25.85, "lng_max": -80.13 },
  "maxPlaces": 1000
}
```

That is Miami restaurants, capped at 1,000 businesses.

***

### How it works — two passes

**Pass 1 — census.** One request per 1-degree cell. It classifies every cell as dense,
border, foreign, exhausted or empty. This is the planning tool: Venezuela is 180 cells
and was classified in **172 requests, about 20 seconds**, showing that only **11.7%** of
the country needed any deeper work. Run it alone with **Census only** to learn what an
area costs *before* spending anything.

**Pass 2 — quadtree.** Only the saturated cells get split into four and retried, level
by level, until they come back under the threshold. Cells that turned out to belong to a
neighbouring country are dropped instead of split — on Venezuela that alone saved about
682 wasted requests.

***

### What you get

One row per business, deduplicated by `feature_id`, Google's own stable identifier, so
the same place never appears twice — not even across separate runs.

`place_id` is **the same identifier the official Places API uses**, so this dataset
joins to anything you already have from Google without extra work.

#### Fill rates — measured, and they depend on the market

Two independent measurements, both on 3 Sep 2026: **12,693 businesses across 10 regions
and 12 verticals** in a Latin American market, and **4,032 businesses across 8 cities**
in the US, Canada, Mexico, Colombia, Peru, Chile and Argentina.

| Field | Toronto | Miami | Mexico City | Remote regions |
|---|---:|---:|---:|---:|
| `name`, `address`, `latitude`, `longitude`, `place_id` | 100% | 100% | 100% | 100% |
| `phone_e164` | 96.7% | 77.7% | 68.2% | **41.4%** |
| `opening_hours` | 96.1% | 74.3% | 81.9% | **48.9%** |
| `rating` | 97.5% | 77.0% | 92.0% | **55.5%** |
| `website` | 87.4% | 74.0% | 40.0% | **5.7%** |
| `reviews_count` | 0.4% | 11.6% | 0.0% | 0.3% |

**Coverage is a property of the market, not of the tool.** The same field runs from
96.7% in Toronto to 41.4% in a remote region. Those numbers are here so you can decide
before you buy, not after.

By vertical, measured: dentists return a phone on **99.5%** of rows and law firms on
**100%**; gyms on only **28.0%**, because many publish a booking site instead.

#### What this Actor does **not** give you

- **A review count you can rely on.** Google serves it only in some responses:
  `reviews_count` came back on **8.5%** of rows. It is delivered when present and it is
  never promised. If you need it on every row, this is not the tool.
- **Review text.** Out of scope: it is writing by identifiable people.
- **Email addresses.** Google Maps does not publish them. Measured: zero email strings
  in 498 records across three verticals, and none on the place page either. Any tool
  promising emails is getting them from somewhere else.
- **A postal code field.** It is embedded inside `address` and its format varies by
  region; parsing it out would be guesswork, so it is left where Google put it.
- **Individual place pages.** Phone, hours and website already come in the search
  response, so fetching each place would multiply the cost by about 200 for nothing.

***

### Pricing — one price, no add-ons

Charged per business delivered, with a lower price on every Apify plan tier:

| Your plan | Per place |
|---|---|
| Free | $0.0010 |
| Bronze | $0.0008 |
| Silver | $0.0006 |
| Gold and above | $0.0005 |

**Phone, opening hours, website, rating, categories and coordinates are included at
that price.** There are no add-on charges for applying a filter or for pulling place
details — a habit in this category that can double the real cost per row.

Rows served from cache are charged the same and always declare their age through
`from_cache` and `data_age_hours`. **Failed tiles are never charged**, and the tiling
overhead — the many requests it takes to break the 200-result cap — is absorbed by the
Actor, not billed to you.

***

### Reading the run summary

`RUN_SUMMARY` in the key-value store is part of the output, not telemetry:

| Field | What it means |
|---|---|
| `census_complete` | `false` means pass 1 did not finish. What you got is the corner of the grid the sweep started from, not a census of the area |
| `tiles_unresolved` | above zero means **this census has gaps**, and the log says so |
| `max_depth_reached` vs `max_depth_limit` | equal means the census is **truncated** — raise the depth or split the area |
| `stop_reason` | `max_places`, `request_budget` or `timeout` — which brake stopped the run |
| `places_from_cache` | how many rows were served without touching Google |

A census with gaps is legitimate. A census with gaps that does not say so is not, and
this Actor will always say so.

***

### Large areas: use batches

A big country does not fit in one run — not because of cost but because of the run time
limit. Run pass 1 first, then run pass 2 in batches with a `requestBudget` and a `bbox`
per batch.

Batches are independent, resumable and free to retry: tiles already exhausted are
skipped, so **re-running a finished batch spends no requests at all**. Measured: a
second run of the same Venezuelan census needed **8 requests instead of 180**.

***

### Country scope

The Actor is global by construction — it takes a bounding box, not a country — but it
ships with a catalogue of 24 countries and **refuses to run on 8 of them**: Spain,
Portugal, France, Germany, Italy and the United Kingdom (GDPR / UK GDPR), Brazil (LGPD)
and Cuba (regime not reviewed).

This is a deliberate limit enforced in code, not an oversight. Aggregating name, phone
and coordinates for hundreds of thousands of small businesses produces a personal-data
file, and those regimes require a legal basis this Actor does not document.

**Whoever runs this Actor is the data controller for the output.** Removal requests:
`privacy@actorstack.dev`.

***

### Notes

**Language changes the data, not the interface.** The language comes from the country
and sets the language of `categories` and `opening_hours`, so two runs of the same area
in different languages produce **different rows**. The language used is in every row as
`language`.

**Place names are kept exactly as Google publishes them**, in their own alphabet and
with their own accents. `Mérida` is not `Merida` and `Kraków` is not `Krakow`.

**Where it runs from.** The Actor routes through a residential proxy. This is not an
optimisation: Google returns HTTP 302 to Apify's own IP ranges, measured on 3 Sep 2026,
so a direct run from the platform returns nothing at all.

# Actor input Schema

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

Supplies the bounding box, the country bias and the language. Countries under GDPR, UK GDPR and LGPD are refused by design — see the README section "Country scope".

## `categories` (type: `array`):

One term per category, in the country's language. Terms barely overlap — measured at 5.5% across 30 of them — so each one you add is close to a full extra pass, in both time and cost.

## `bbox` (type: `object`):

Overrides the country's own box, so you can scrape a single city or run one batch of a large country. Four decimal degrees, south-west corner first — for Miami: {"lat\_min": 25.70, "lng\_min": -80.32, "lat\_max": 25.85, "lng\_max": -80.13}. Leave it empty to sweep the whole country.

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

Stop after this many unique businesses. 0 means no limit. This is the safest way to cap what a run costs.

## `requestBudget` (type: `integer`):

Maximum requests for this run. A whole country does not fit in one run — split it into batches. Tiles already visited are skipped for free, so re-running continues where the last one stopped instead of starting over.

## `censusOnly` (type: `boolean`):

Runs only the cheap first pass: one request per 1-degree cell. It tells you which areas are dense, which belong to a neighbouring country and which are empty — so you know what a country costs before spending it. Venezuela is 180 cells and classifies in about 20 seconds.

## `useCache` (type: `boolean`):

Skip tiles that an earlier run already exhausted. Their businesses are still delivered, flagged with from\_cache and data\_age\_hours so a cached row never passes as a fresh one.

## `maxDepth` (type: `integer`):

A safety guard, NOT the stopping rule — the stopping rule is saturation. If a run reaches this depth its census is truncated, and both the log and the run summary say so. A dense 1-degree cell over a capital was still splitting at depth 8.

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

A tile counts as full at this many results and gets split into four. 190 leaves margin under Google's hard ceiling of about 200, because the same query does not return the same count twice.

## `gridStep` (type: `integer`):

Size in degrees of the first-pass cells. 1 degree is the measured default; larger cells classify faster but hide more inside each one.

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

Parallel requests. 10 is the measured default; 617 requests at 12 ran without a single block.

## Actor input object example

```json
{
  "country": "US",
  "categories": [
    "restaurant",
    "pharmacy"
  ],
  "maxPlaces": 0,
  "requestBudget": 5000,
  "censusOnly": false,
  "useCache": true,
  "maxDepth": 8,
  "saturationThreshold": 190,
  "gridStep": 1,
  "maxConcurrency": 10
}
```

# Actor output Schema

## `dataset` (type: `string`):

Every business found inside the area, deduplicated by feature\_id. Includes phone, opening hours, website, rating, categories and coordinates at no extra charge.

## `contacts` (type: `string`):

The same rows narrowed to phone, website and address. Phone coverage is a property of the market: 96.7% in Toronto, 77.7% in Miami, 41.4% in remote regions — all measured.

## `geo` (type: `string`):

Coordinates, plus code and the tile each business was discovered in, so map coverage can be audited rather than trusted. Latitude and longitude are on 100% of rows.

## `runSummary` (type: `string`):

Whether pass 1 finished, how many tiles were left unresolved, the depth reached, which brake stopped the run, and how much came from cache. If census\_complete is false or tiles\_unresolved is above zero, this census has declared gaps.

# 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 = {
    "categories": [
        "restaurant",
        "pharmacy"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("oswaldocarabano/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 = { "categories": [
        "restaurant",
        "pharmacy",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("oswaldocarabano/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 '{
  "categories": [
    "restaurant",
    "pharmacy"
  ]
}' |
apify call oswaldocarabano/google-maps-scraper --silent --output-dataset

```

## MCP server setup

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