# Google Maps Scraper: Places, Contacts & Coordinates (`arman-bd/google-maps-scraper`) Actor

Search Google Maps by keyword and area. Names, categories, addresses, coordinates, phones, websites, ratings and opening hours, each with a stable Google place ID. No API key, no billing account.

- **URL**: https://apify.com/arman-bd/google-maps-scraper.md
- **Developed by:** [Arman Hossain](https://apify.com/arman-bd) (community)
- **Categories:** Lead generation, Business, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.40 / 1,000 place scrapeds

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 Scraper: Places, Contacts & Coordinates

![Google Maps Scraper: Places by keyword and area, addresses, coordinates, phones, websites, ratings and hours](https://api.apify.com/v2/key-value-stores/ZQOcNAOHrIgTacAmy/records/google-maps-scraper.jpg)

**Google Maps Scraper** searches Google Maps by keyword and area and returns one clean row per business: name, categories, a split address, coordinates, phone in two formats, website, rating, opening hours and accessibility attributes.

Every row carries `placeId`, Google's stable identifier, so results join across runs and against your own records. **No API key, no billing account, no proxy setup and no browser.**

**Agent skill: [SKILL.md](https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/google-maps-scraper.md)**

```
https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/google-maps-scraper.md
```

### What you get

| Output field | Meaning |
|---|---|
| `placeId` | Google's stable place ID, `ChIJ…`. Use it as your join key |
| `cid`, `fid` | Google's internal identifiers for the same place |
| `kgmid` | Knowledge Graph ID, `/g/…` or the legacy `/m/…`, joins to Google's entity graph |
| `title` | Business name |
| `categoryName`, `categories` | Primary category, and every category Google files it under |
| `address` | Formatted single-line address |
| `street`, `city`, `postalCode`, `countryCode` | Address components as Google publishes them, not split out of the display string |
| `neighborhood`, `locality` | `Mitte`, and `Berlin, Germany` |
| `latitude`, `longitude` | Coordinates |
| `timezone` | IANA zone, e.g. `Europe/Berlin` |
| `phone`, `phoneNational` | E.164 and the locally formatted number |
| `website`, `domain` | The listed website and its host |
| `rating` | Star rating, always a number |
| `reviewsCount`, `priceLevel` | Always `null`. Google does not publish either in search results |
| `permanentlyClosed`, `temporarilyClosed` | Closure flags |
| `openingHours` | Per day: the display text plus a machine-comparable `interval` of hour/minute pairs |
| `attributes` | `{group, name, available}`. **Read `available`** — Google publishes absence as its own entry |
| `editorialSummary` | Google's one-line blurb, where it has written one |
| `bookingLinks` | Reservation URLs, where the place has them |
| `imageUrl` | Thumbnail |
| `url` | Google Maps link built from `placeId` |
| `searchTerm`, `gridCell`, `position` | Which input produced the row, which grid cell it came from, and where it ranked |
| `scrapedAt` | Run timestamp |

A `RUN_SUMMARY` record in the key-value store holds the resolved centre point, places saved, what each filter removed, duplicates collapsed, whether each term exhausted its area, and any field that failed its shape check.

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `searchTerms` | array | required | What you would type into the Maps search bar. Each term is searched independently across the whole area |
| `location` | string | | Free text, e.g. `Berlin, Germany`. Resolved once per run to a centre point, which is reported in `RUN_SUMMARY` |
| `latitude`, `longitude` | number | | Explicit centre point. Overrides `location`. Use when a place name is ambiguous |
| `radiusKm` | integer | `5` | How far around the centre to search. Smaller is denser and more local; larger covers more ground with less depth |
| `maxPlacesPerTerm` | integer | `200` | Cap per term. The main cost control, since charging is per place |
| `gridSize` | integer | `1` | Split the area into N×N cells and search each. `2` finds substantially more places at four times the requests |
| `minRating` | number | | Keep only places at or above this rating. **Also removes unrated places** |
| `skipClosed` | boolean | `false` | Drop permanently and temporarily closed places |
| `websiteFilter` | string | `all` | `all`, `withWebsite`, or `withoutWebsite` |

Either `location` or a `latitude`/`longitude` pair is required. There is no global Maps search, so a run without an area fails with a named error rather than returning nothing.

```json
{
 "searchTerms": ["specialty coffee"],
 "location": "Berlin, Germany",
 "radiusKm": 10,
 "maxPlacesPerTerm": 200,
 "gridSize": 2,
 "minRating": 4.0
}
```

### Output

One record per unique place. Every field is present on every record; `null` means Google does not publish it for that place, never that it was skipped.

```json
{
 "placeId": "ChIJnS0AQjdRqEcRJk8cf-B4y1M",
 "cid": "0x47a8513742002d9d:0x53cb78e07f1c4f26",
 "kgmid": "/g/11s5wzp9md",
 "title": "Fauve Coffee Berlin",
 "categoryName": "Coffee shop",
 "categories": ["Coffee shop", "Cafe", "Coffee roastery"],
 "address": "Fauve Coffee Berlin, Neue Schönhauser Str. 8, 10178 Berlin, Germany",
 "street": "Neue Schönhauser Str. 8",
 "city": "Berlin",
 "postalCode": "10178",
 "countryCode": "DE",
 "neighborhood": "Mitte",
 "locality": "Berlin, Germany",
 "latitude": 52.5247417,
 "longitude": 13.404067,
 "timezone": "Europe/Berlin",
 "phone": null,
 "phoneNational": null,
 "website": null,
 "domain": null,
 "rating": 4.7,
 "reviewsCount": null,
 "priceLevel": null,
 "permanentlyClosed": false,
 "temporarilyClosed": false,
 "openingHours": [{ "day": "Saturday", "hours": "11 AM–6:30 PM", "interval": [[11], [18, 30]] }],
 "attributes": [{ "group": "Accessibility", "name": "Wheelchair accessible parking lot", "available": false }],
 "editorialSummary": null,
 "bookingLinks": null,
 "imageUrl": "https://lh5.googleusercontent.com/-OaxK-r1h6zM/…/photo.jpg",
 "url": "https://www.google.com/maps/place/?q=place_id:ChIJnS0AQjdRqEcRJk8cf-B4y1M",
 "searchTerm": "specialty coffee",
 "gridCell": "r1c1",
 "position": 1,
 "scrapedAt": "2026-08-08T21:04:11.000Z"
}
```

### Use cases

- **Local lead lists.** Every business of a category in a metro area, with phone and website attached.
- **Prospecting for businesses without a website.** `websiteFilter: "withoutWebsite"` is the whole pitch for a web agency, and it is one field.
- **Geocoding and enrichment.** Turn a category into rows with coordinates, a timezone and a stable ID that joins to anything else keyed on Google places.
- **Coverage and reputation tracking.** Run on a schedule and watch ratings, hours and new entrants move.
- **Territory planning.** `gridSize` with a wide `radiusKm` gives even coverage of a city rather than whatever ranks for one centre point.

### Limits and behaviour

- **Results churn about 10% between runs.** The same query returns a roughly 90% stable set of place IDs; Google reorders its own results. Join on `placeId` and treat a single absence as noise, not a closure.
- **How many places a term yields depends on density**, not a fixed cap. A dense term in a large city paginates into the hundreds; a quiet suburb returns a handful. When a term exhausts its area, `RUN_SUMMARY.ceilingHit` says so — raise `radiusKm` or `gridSize` rather than `maxPlacesPerTerm`.
- **`reviewsCount` and `priceLevel` are always `null`.** Google does not publish them in search results. They are declared so the column exists rather than appearing later and breaking your pipeline.
- **Attributes can be negative.** `{"name": "Wheelchair accessible entrance", "available": false}` means the place does **not** have it. Reading `name` without `available` inverts the meaning.
- **Grid cells overlap.** Roughly a sixth to a third of what a grid fetches is a repeat. Duplicates are collapsed on `placeId` and charged once; `RUN_SUMMARY.duplicatesCollapsed` reports the count.
- **`minRating` also removes unrated places.** Leave it unset to keep them.
- **Address components are `null` where Google does not publish them**, never guessed. `state` is absent in most of the world and is not included.
- **No reviews, photo galleries or email enrichment.** Those are separate products.
- **Public data only.** No authentication, and nothing that gets around access controls.

### FAQ

**Do I need a Google API key or a billing account?** No. You supply no credentials.

**Why did I get fewer places than `maxPlacesPerTerm`?** The area ran out of matches. Check `ceilingHit` in `RUN_SUMMARY`: if it is `true`, widen the area instead of raising the cap.

**What is `placeId` for?** It is Google's stable identifier for a business. Use it to join runs, deduplicate against your own records, and build a Maps link.

**How do I cover a whole city?** Raise `radiusKm` to cover it and set `gridSize` to 2 or 3. Cells overlap, and duplicates are charged once.

**Why do two runs return slightly different places?** Google reorders results per request. See Limits and behaviour.

**Can I filter by number of reviews?** No. The review count is not in the data this Actor reads, so such a filter would silently match nothing.

**Can I plug it into something else?** Yes. Apify API, the client libraries, webhooks, scheduled runs, dataset exports to JSON, CSV or Excel, or MCP. The output is structured JSON.

### API example

```bash
curl -X POST "https://api.apify.com/v2/acts/arman-bd~google-maps-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
 -H "Content-Type: application/json" \
 -d '{
 "searchTerms": ["dentist"],
 "location": "Austin, TX",
 "radiusKm": 15,
 "gridSize": 2,
 "websiteFilter": "withoutWebsite"
 }'
```

### JavaScript example

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('arman-bd/google-maps-scraper').call({
 searchTerms: ['specialty coffee'],
 location: 'Berlin, Germany',
 radiusKm: 8,
 minRating: 4.0,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const p of items) console.log(`${p.title}, ${p.rating}, ${p.city} — ${p.website ?? 'no website'}`);
```

# Actor input Schema

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

What you would type into the Maps search bar — 'dentist', 'specialty coffee'. Each term is searched independently across the whole area, so two terms cost roughly twice one.

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

Free-text area to search, e.g. 'Berlin, Germany' or 'Austin, TX'. Resolved once per run to a centre point, which is written into the run summary so you can check it landed where you meant.

## `latitude` (type: `number`):

Explicit centre point latitude. Overrides 'Location' when given together with longitude. Use it when a place name is ambiguous.

## `longitude` (type: `number`):

Explicit centre point longitude. Must be given together with latitude, otherwise 'Location' is used instead.

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

How far around the centre point to search. Sets the map viewport: a smaller radius returns denser, more local results, a larger one covers more ground with less depth.

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

Cap on places returned for each search term. One request returns up to about 200; a dense term paginates to several hundred. This is the main cost control, since you are charged per place.

## `gridSize` (type: `integer`):

Split the area into a grid of this many cells per side and search each one. 1 is a single search. 2 means 4 searches and typically finds substantially more places, at 4x the requests and with real overlap - duplicates are collapsed and charged once. Maximum 4.

## `minRating` (type: `number`):

Keep only places rated at or above this, e.g. 4.5. Places with no rating at all are also removed, which surprises people - leave this empty to keep unrated places.

## `skipClosed` (type: `boolean`):

Remove places marked permanently or temporarily closed, so the result set is limited to businesses currently operating.

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

Keep every place, only those that list a website, or only those without one. 'Without a website' is the usual choice for agency prospecting.

## Actor input object example

```json
{
  "searchTerms": [
    "dentist",
    "specialty coffee"
  ],
  "location": "Austin, TX",
  "radiusKm": 5,
  "maxPlacesPerTerm": 200,
  "gridSize": 1,
  "skipClosed": false,
  "websiteFilter": "all"
}
```

# Actor output Schema

## `items` (type: `string`):

Every place the run produced.

## `runsummary` (type: `string`):

The RUN\_SUMMARY record from the run's key-value store.

# 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": [
        "specialty coffee"
    ],
    "location": "Berlin, Germany"
};

// Run the Actor and wait for it to finish
const run = await client.actor("arman-bd/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 = {
    "searchTerms": ["specialty coffee"],
    "location": "Berlin, Germany",
}

# Run the Actor and wait for it to finish
run = client.actor("arman-bd/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 '{
  "searchTerms": [
    "specialty coffee"
  ],
  "location": "Berlin, Germany"
}' |
apify call arman-bd/google-maps-scraper --silent --output-dataset

```

## MCP server setup

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