# Google Maps Business Scraper (`savefy/gmaps-business-scraper`) Actor

Finds businesses Google's English-only search misses -- term expansion into Malay, Chinese, Japanese, German, Spanish, Portuguese, French, verified with real sweeps in five markets. Also fast, cheap, reliable: plain HTTP, no browser, resumable sweeps, proxy rotation, degraded-session detection.

- **URL**: https://apify.com/savefy/gmaps-business-scraper.md
- **Developed by:** [Savefy](https://apify.com/savefy) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 results

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 Business Scraper** finds businesses that Google's English-only search misses. An optional local-language expansion feature widens a single search term into its equivalents across seven other languages — Malay, Chinese, Japanese, German, Spanish, Portuguese, French — and every one of them was verified with a real sweep through this Actor before shipping, not just machine-translated and hoped for. In our own testing — one small sweep, three business types (bakery, pharmacy, gym), Tokyo only — roughly 3 in 10 unique results found via the Japanese terms weren't reachable through the bare English word at all. That's an observation from that one run, not a general performance claim; your mileage will vary by language, city, and business type. Useful, but not required either way — your own term list always works just as well.

It also pulls business data directly from Google Maps' own search endpoint over plain HTTP — no browser, no Google API key — which keeps it fast and cheap to run at scale. The sweep is built for reliability over long runs: it resumes from where a previous run left off instead of restarting from zero, and it rotates to a fresh proxy session for every search term. It also catches something most scrapers miss: Google's session warm-up can fail silently, returning a "successful" run that's quietly stripped of review counts and other fields. This Actor detects that pattern automatically and retries on a fresh session before it reaches your dataset.

Built on the Apify platform, so you get scheduled runs, API/webhook access to results, dataset export (JSON/CSV/Excel), and proxy rotation without managing any of that yourself.

### Why use Google Maps Business Scraper?

- **Finds what English misses.** Local-language term expansion across 8 languages (English, Malay, Chinese, Japanese, German, Spanish, Portuguese, French), each verified with a real sweep before shipping — not machine-translated and hoped for.
- **No API key, no billing surprises.** This hits the same endpoint the Google Maps website itself uses, not the paid Places API.
- **Resumable by design.** Every `(term, tile)` pair swept is remembered. Re-run with the same terms/area/spacing and already-covered ground is skipped, not re-fetched — a run that gets interrupted or migrated loses nothing.
- **Built for wide sweeps, not single lookups.** Give it a metro area and a dozen search-term variants and it tiles the whole area, subdividing only where results are actually dense — not a fixed grid burning requests on empty countryside.
- **Niche-agnostic.** No hardcoded categories, no per-industry logic. Works the same for sports facilities, restaurants, repair shops, or anything else you can describe as a search term.

### How to use Google Maps Business Scraper

1. Open the Input tab and add one or more **Search terms** — different phrasings/languages of the same thing find different businesses, so a handful of variants beats one "perfect" term.
2. Set a **Center point + radius**, or an explicit **Bounding box** for more control over the exact area.
3. Leave **Tile spacing**, **Max subdivision depth**, and **Max pages per tile** at their defaults for a first run — they control the coverage-vs-request-count tradeoff, and defaults are a reasonable starting point.
4. Turn on **Apify Proxy** (on by default, residential recommended) — Google rate-limits aggressively by IP, and this Actor rotates proxy sessions per search term to spread load across a long sweep.
5. Run it. Check the Output tab, or pull results via the API/dataset export once it finishes (or partway through — items land in the dataset as they're found, not just at the end).

### Input

| Field | Description |
|---|---|
| `searchTerms` | The terms to sweep, e.g. `["bakery", "coffee shop"]`. Required. |
| `boundingBox` | Explicit `{north, south, east, west}` area. Takes precedence over center+radius if both are set. |
| `centerLat` / `centerLng` / `radiusKm` | Simpler alternative to a bounding box — a square area around a point. |
| `tileSpacingKm` | Distance between tile centers. Smaller = finer coverage, more requests. |
| `maxTileDepth` | How many times a saturated tile can split into 4 half-spacing tiles. |
| `maxPagesPerTile` | Page cap per (term, tile), 20 results/page. |
| `requestDelaySecs` | Politeness delay between requests. |
| `countryCode` / `languageCode` | Google's `gl`/`hl` parameters. |
| `expandTerms` / `expansionLanguages` / `customTerms` | Optional: expands each term into local-language equivalents (English, Malay, Chinese, Japanese, German, Spanish, Portuguese, French) plus business-noun suffixes (centre, court, club, …). |
| `proxyConfiguration` | Apify Proxy settings. Strongly recommended for anything beyond a small test. |

See the Input tab for full descriptions, defaults, and validation.

### Output

One dataset item per business found. Example:

```json
{
    "name": "Sunrise Bakery",
    "place_id": "ChIJ...",
    "feature_id": "0x89c259a...:0x...",
    "latitude": 40.7484,
    "longitude": -73.9857,
    "rating": 4.6,
    "review_count": 312,
    "website": "https://example.com",
    "website_host": "example.com",
    "phone": "+12125551234",
    "neighbourhood": "Midtown Manhattan",
    "full_address": "123 Example Ave, New York, NY 10001, USA",
    "categories": ["Bakery", "Cafe"],
    "gcids": [],
    "found_via": "bakery"
}
```

You can download the dataset in various formats such as JSON, HTML, CSV, or Excel.

#### Data fields

| Field | Meaning |
|---|---|
| `name`, `full_address`, `neighbourhood` | Business identity and location |
| `latitude`, `longitude` | Coordinates |
| `rating`, `review_count` | Google's rating and review count, when available |
| `categories` | Google's own display-label categories for the business |
| `gcids` | Google's internal category slugs — usually empty; only served to a logged-in session, which this Actor doesn't use |
| `website`, `website_host`, `phone` | Contact info, when listed |
| `place_id`, `feature_id` | Google's stable identifiers for the business |
| `found_via` | Which of your search terms surfaced this row. Provenance only — not a quality or category signal, since the same business can be found by several terms |

### Cost estimation

Cost scales with tiles × terms × pages, and with whether a tile saturates and subdivides. A calibration run — a handful of terms, `maxTileDepth: 0`, coarse spacing — costs a few dozen requests and a couple of minutes of compute. A full metro sweep with subdivision enabled can run into the thousands of requests; run a `maxTileDepth: 0` pass first to see which terms actually saturate before turning subdivision on for those specifically. Term expansion (`expandTerms`) can multiply your term count 10-40x — check the run log's up-front cost estimate before letting a large expansion run unattended.

### Tips

- Start with `maxTileDepth: 0` and coarse `tileSpacingKm` to see which terms saturate before spending requests on subdivision.
- Vocabulary — more search term variants, including local-language ones — matters more than finer tiling for coverage. A few extra terms often find more than halving the tile spacing does.
- If you turn on `expandTerms`, also turn on `proxyConfiguration` (on by default) — expansion multiplies request volume, and Google throttles a single un-rotated IP fast under that load.
- If a run needs to resume cleanly after being stopped, just re-run with the exact same `searchTerms`, area, and `tileSpacingKm` — already-swept tiles are skipped automatically.

### FAQ

**Is this legal?** This Actor queries a public endpoint the Google Maps website itself uses, over plain HTTP with no authentication bypass. You're responsible for complying with Google's Terms of Service and applicable law in how you use the results.

**Why are `gcids` almost always empty?** Google only returns its internal category slugs to a logged-in session. This Actor deliberately doesn't authenticate as a Google account to fetch them — `categories` (the display-label field, populated on essentially every row) is the reliable classification signal instead.

**Some businesses are missing review counts / categories entirely.** This usually means a cookie/session issue on Google's side for that request, not a bug in field parsing — the Actor detects this pattern (a session whose first tile comes back with businesses but no review counts at all) and automatically rotates to a fresh session and retries. If it still happens on isolated tiles, re-running (it resumes, not restarts) usually picks them up on a different session.

**Which languages does term expansion support?** English, Malay, Chinese, Japanese, German, Spanish, Portuguese, and French — each verified with a real sweep through this Actor (not just machine-translated) before shipping. Languages without empirical verification aren't included, on the theory that a wrong translation is a silent, undiagnosable zero-result search — worse than no coverage at all.

**Found a bug or have a feature request?** Use the Issues tab on this Actor's page.

# Actor input Schema

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

Every term is swept independently across the whole tile grid. Overlap between terms is expected and fine — results are deduped by place\_id within the run. Multiple languages/phrasings of the same thing are the main coverage lever (more so than tile spacing), so include synonyms and local-language variants here.

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

Explicit area as {"north":.., "south":.., "east":.., "west":..} in decimal degrees. Takes precedence over Center point + radius below if both are set. Leave empty to use the center point instead.

## `centerLat` (type: `number`):

Used with Center longitude + Radius to build the search area, if Bounding box is not set.

## `centerLng` (type: `number`):

Used with Center latitude + Radius to build the search area, if Bounding box is not set.

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

Distance from the center point to each edge of the search area (a square, not a circle — tiling is grid-based). Only used when Bounding box is not set.

## `tileSpacingKm` (type: `integer`):

Distance between tile centers. Smaller spacing = more, narrower tiles = more requests but finer coverage in dense areas. Start coarse (e.g. 40km) to gauge a term's yield, then narrow on the terms that saturate.

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

How many times a saturated tile (full page of results AND several genuinely new businesses) can be split into 4 half-spacing tiles. 0 disables subdividing — useful for a first calibration pass.

## `minNewToSplit` (type: `integer`):

A full-page tile only gets subdivided if it also yielded at least this many businesses not already seen this run. Prevents wasting requests re-splitting sparse areas that Google just padded with distant results.

## `maxPagesPerTile` (type: `integer`):

Page cap per (term, tile) before moving on, at 20 results/page.

## `requestDelaySecs` (type: `number`):

Politeness delay between page/tile fetches.

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

Google's 'gl' country-bias parameter, e.g. 'my' for Malaysia, 'us' for United States.

## `languageCode` (type: `string`):

Google's 'hl' interface-language parameter, e.g. 'en', 'ms', 'zh-CN'. Affects which language result text comes back in, not which businesses are found — use multiple search terms for that.

## `expandTerms` (type: `boolean`):

`gl`/`hl` only bias region and UI language — they do NOT translate your query. Searching "laundry" with gl=my searches the literal English word and misses every "kedai dobi". When this is on, each search term (and Custom term) is matched against a business-type table and expanded into that business's equivalents across Expansion languages below, e.g. "laundry" → "dobi", "kedai dobi", "洗衣店", "自助洗衣". A term with no match in the table is kept as-is and logged, never dropped. English-language results are then also expanded with business-noun suffixes (centre, court, club, …), e.g. "padel" → "padel centre", "padel court" …

## `expansionLanguages` (type: `array`):

Which languages to expand into when Expand terms is on. Table coverage: en, ms, zh, ja, de, es, pt, fr — every one empirically verified with a real sweep through this Actor before shipping. th/id are not in the table (translations were never verified), so listing them here currently has no effect. Leave empty to derive from Country code (e.g. my → en, ms, zh; jp → en, ja; de → en, de; es → en, es; br → en, pt; fr → en, fr).

## `customTerms` (type: `array`):

Extra search terms merged in alongside Search terms — always searched, regardless of Expand terms. Use this for anything the business-type table doesn't cover; an unmatched term is searched as-is rather than dropped.

## `expansionSuffixes` (type: `array`):

Overrides the default business-noun suffix list. Only applied to plain-ASCII (English) terms — appending an English suffix to a non-Latin-script or Malay-only term produces an unsearchable string.

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

Google rate-limits aggressively by IP. Apify Proxy (residential recommended) is strongly advised for anything beyond a small calibration run.

## Actor input object example

```json
{
  "searchTerms": [
    "badminton court"
  ],
  "centerLat": 40.758,
  "centerLng": -73.9855,
  "radiusKm": 2,
  "tileSpacingKm": 20,
  "maxTileDepth": 0,
  "minNewToSplit": 5,
  "maxPagesPerTile": 1,
  "requestDelaySecs": 2,
  "countryCode": "us",
  "languageCode": "en",
  "expandTerms": false,
  "expansionSuffixes": [
    "centre",
    "center",
    "court",
    "club",
    "complex",
    "hall",
    "arena"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `results` (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": [
        "badminton court"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("savefy/gmaps-business-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": ["badminton court"] }

# Run the Actor and wait for it to finish
run = client.actor("savefy/gmaps-business-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": [
    "badminton court"
  ]
}' |
apify call savefy/gmaps-business-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,savefy/gmaps-business-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/44e4y43pv0TixSvCV/builds/aqQfsHtgrOD4UaXnu/openapi.json
