# Google Maps Scraper — Complete Business Records (`thenetaji/google-maps-scraper`) Actor

Build a list of businesses from Google Maps. Search around any point on the map and every result arrives as a finished record: name, address, coordinates, phone in two forms, website, categories, rating, opening status, attributes and photos. No follow-up lookup per business.

- **URL**: https://apify.com/thenetaji/google-maps-scraper.md
- **Developed by:** [The Netaji](https://apify.com/thenetaji) (community)
- **Categories:** Lead generation, Travel, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.20 / 1,000 businesses

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/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

A list of businesses from Google Maps with the contact details already on it. A search term and a
point on the map go in, and every business that comes back is a finished record: the phone number,
the website, the full address, the coordinates, the categories, the star rating, whether the place is
open right now, what it offers, and its photos. There is no second lookup per business, and no
per-business detail charge, because there is nothing left to look up.

### What every row carries

| Field | What it holds |
| --- | --- |
| `name` | Business name, as Google files it |
| `cid` | Google's own id for the place, and what both sibling Actors take as input |
| `place_id`, `feature_id` | Google's `ChIJ…` id and its Knowledge Graph id, for joining outward |
| `address`, `address_lines`, `address_components` | The address as one line, as display lines, and as Google's own structured list |
| `neighborhood`, `city`, `country_code` | Where Google files the business |
| `latitude`, `longitude`, `timezone` | Where it actually is, and the zone its hours are stated in |
| `categories` | What Google says it is, most specific first |
| `rating` | Star rating out of 5 |
| `phone`, `phone_e164` | The number as displayed, and in the form a dialer or CRM takes |
| `website`, `website_domain` | The site, and the bare domain as a join key |
| `description` | Google's editorial blurb, where it has written one |
| `hours_status`, `hours_today` | Google's own open/closed sentence, and the current day's spans |
| `attributes` | Accessibility, amenities, payment and service options, in Google's own groups |
| `photos` | Up to ten photo links, cover first |
| `maps_url` | Google's own link to the place |

Coverage was counted across 20 businesses rather than assumed. `name`, `cid`, `place_id`, `address`,
`latitude`, `longitude`, `rating`, `categories`, `photos` and `timezone` were present on 20 of 20;
`website` and `attributes` on 19 of 20; `phone` on 15 of 20. `website` is therefore the contact field
most often filled in, and reading it beside `phone` rather than instead of it is the difference
between 19 usable rows and 15.

### Accepted input

`query` is required and is what to look for: a category such as `coffee` or `dentist`, or a business
name. It is Google's own search box, so Google's reading of a phrase applies.

`latitude` and `longitude` are both required and are the centre of the map view to search. There is
no place-name parameter, and its absence is deliberate: Google honours the coordinates rather than
resolving a city out of the query. The same query recentred on San Francisco, Austin and Miami
returned local businesses each time, at median distances of 2.3 km, 2.6 km and 4.1 km from the point
asked for.

`span_metres` is how wide a map view to search and defaults to `20000`. It behaves as a reach dial
rather than a hard boundary; the measurements are in *Depth, and how to go past it*.

`area_metres` defaults to `0`, which searches the single map view at the point given. A larger value
sweeps a square of that width as a grid of map views.

`maxItems` caps how many rows the run saves and defaults to 100. Setting it to `0` removes the cap,
in which case the run ends when the area is exhausted.

```json
{
  "query": "coffee",
  "latitude": 37.7749,
  "longitude": -122.4194,
  "span_metres": 2000,
  "area_metres": 6000,
  "maxItems": 500
}
```

### Response fields

```json
{
  "cid": "0x808587fb77f5f64d:0x25d564f177f99946",
  "name": "The Coffee Movement",
  "categories": ["Coffee shop", "Cafe"],
  "rating": 4.8,
  "address": "1030 Washington St, San Francisco, CA 94108",
  "neighborhood": "Chinatown",
  "city": "San Francisco, CA",
  "country_code": "US",
  "latitude": 37.7936,
  "longitude": -122.4116,
  "timezone": "America/Los_Angeles",
  "phone": "(415) 780-1902",
  "phone_e164": "+14157801902",
  "website": "https://www.thecoffeemovement.com/",
  "website_domain": "thecoffeemovement.com",
  "hours_status": "Closed · Opens 8 AM Mon",
  "hours_today": ["9 AM–2 PM"],
  "attributes": { "Amenities": ["Restroom", "Wi-Fi"] },
  "photos": ["https://lh3.googleusercontent.com/p/AF1QipN-example=w408-h306-k-no"],
  "place_id": "ChIJ_2f19_uHhYARRpn5d_Fk1SU",
  "maps_url": "https://www.google.com/maps/place/?q=place_id:ChIJ_2f19_uHhYAR"
}
```

Fields Google states nothing for are returned as null rather than omitted, so every row has the same
shape. `cid` is Google's own identifier for the place and is the value both sibling Actors take as
input, so nothing has to be constructed from a URL. `place_id` is the `ChIJ…` form the official
Places API speaks, published because it joins these rows to anything already built against Google's
own API.

### Depth, and how to go past it

One query at one map view runs out at about 182 distinct businesses. That was measured by identifier
set rather than by row count: ten pages carried 200 rows holding 182 unique places, so the pages
overlap by roughly ten per cent. Duplicates are removed within a run by `cid`, including across
tiles, so a row is saved once no matter how many views returned it.

That ceiling belongs to one question rather than to the site, because the view is a dial. Holding
everything else fixed on one query in San Francisco:

| `span_metres` | Median distance of a result | Furthest result |
| --: | --: | --: |
| 2,000 | 0.31 km | 0.74 km |
| 20,000 | 2.12 km | 2.91 km |
| 200,000 | 2.29 km | 13.97 km |

So covering an area that holds more than 182 businesses is a matter of asking several narrow views
rather than one wide one. `area_metres` does exactly that: it places map views half a `span_metres`
apart across a square of the requested width, centred on the point given. Half a width is the step
because the furthest result at a 2,000 m view was 0.74 km, and half a width puts the furthest corner
of a grid square 0.354 widths from the nearest centre, which is inside that reach. That reach was
measured where businesses are dense, and a thinner area returns results from further out, so the grid
is conservative rather than optimistic there.

Two narrow queries also reach further than one broad one over the same ground: `coffee` and `pizza`
at the same point returned 21 and 21 businesses with one business in common.

### Questions

**Is a second lookup needed to get phone numbers and websites?**
No, and that is the main difference from the usual shape of a Maps scraper. On this site a search
result and a place-detail response are the same record, verified field by field across both surfaces,
so the search row already carries `phone`, `phone_e164`, `website`, `hours_status`, `attributes`,
`photos` and coordinates.
[Google Maps Place Scraper](https://apify.com/thenetaji/google-maps-place-scraper) exists for
identifiers that arrive from somewhere else, not as a second step after this Actor.

**Why did a run return fewer businesses than `maxItems`?**
Because the area ran out. Each map view is at most ten pages of twenty, the pages overlap, and a
distinct-place count near 182 for a single view is the normal end of one. Raising `area_metres`, or
lowering `span_metres` and raising `area_metres` together, is what widens the harvest; raising
`maxItems` alone does not.

**Why does a large `area_metres` sometimes fail before the run starts?**
Because a single run walks at most 400 map views, and a grid finer than that is thousands of pages
nobody meant to ask for. The request is refused with the arithmetic in the message, naming the area,
the viewport width and the number of views they imply, so the fix is visible: widen the viewport,
shrink the area, or split it across runs.

**Why are the same businesses returned by two different runs over one city?**
Because a wide view returns the same busiest places from wherever it is centred. Narrower views at
several centres are what separate them, which is the reason `span_metres` and `area_metres` are two
inputs instead of one radius.

**Why is a business missing that appears on maps.google.com?**
Most often because the view was centred elsewhere or was too wide to reach it: at a 20,000 m width
the furthest result measured was 2.91 km from the centre. It is also worth reading the run log, which
prints Google's own reading of the query when it differs from what was sent; that distinguishes an
area with no matches from a search that ran for something else.

**Why is there no review count beside `rating`?**
Because the integer in the record that looks like one disagrees with the count Maps displays, reading
1,324 where Maps said 1,723 and 10,337 where Maps said 5,950. It is not published under any name. The
reliable way to reach that number is to count what
[Google Maps Reviews Scraper](https://apify.com/thenetaji/google-maps-reviews-scraper) returns.

**Why does `hours_today` only cover one day?**
Because the full week is not in this payload. Measured across 8 businesses, the hours block held
exactly one day every time, always the current one, and the record contained no slot carrying four or
more weekday names. The field is named for what it actually is. `hours_status` carries Google's own
sentence about the business at the moment of reading, such as `Closed · Opens 8 AM Mon`, and it is
the useful half of the hours because it names the next transition.

**Why is `attributes` an object of varying keys rather than fixed columns?**
Because Google's own groups vary by business and by category. Flattening them into a fixed set of
columns would invent groups a dentist does not have and drop the ones a restaurant does, so the map
is saved as it arrives, keyed on names such as `Accessibility` and `Amenities`.

**Does the run need a Google API key, or count against Places API quota?**
No. No account, key, quota or billing relationship with Google is involved.

### Related Actors

[Google Maps Reviews Scraper](https://apify.com/thenetaji/google-maps-reviews-scraper) takes the
`cid` on every row here and returns that place's reviews, newest first, to the end of the corpus.

[Google Maps Place Scraper](https://apify.com/thenetaji/google-maps-place-scraper) returns the same
business record for identifiers already in hand, which is the right choice for refreshing a stored
list rather than discovering a new one.

# Actor input Schema

## `query` (type: `string`):

What to look for: `coffee`, `dentist`, `hardware store`, or a business name. This is Google's own search box, so its reading of a phrase applies. Two narrow terms cover an area better than one broad one — `coffee` and `pizza` at the same point returned 21 and 21 businesses with one business in common.

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

Latitude of the point to search around. Google searches where you point it rather than reading a city out of the search term: the same term recentred on San Francisco, Austin and Miami returned local businesses each time, at median distances of 2.3, 2.6 and 4.1 km from the point asked for.

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

Longitude of the point to search around. Pair it with **Latitude** — the two are the centre of the map view, and **Viewport width** is how wide a view is taken around it.

## `span_metres` (type: `integer`):

How wide a map view to search, in metres. It behaves as a reach dial: on one term in San Francisco, a span of 2,000 kept every result within 0.74 km of the centre, 20,000 within 2.91 km, and 200,000 within 13.97 km. One term at one viewport runs out at about 182 distinct businesses, so a narrow span asked at several points reaches further than a wide one asked once — that is what **Area to cover** does.

## `area_metres` (type: `integer`):

Leave at 0 to search one map view. Set it to sweep a square of this width, centred on your point, as a grid of map views — the way past the ~182-business limit of a single view. Tile centres are half a **Viewport width** apart, and every business is saved once no matter how many tiles return it. Each map view is up to 10 pages of 20 businesses, so a wide area at a narrow span is a long run.

## `maxItems` (type: `integer`):

Caps how many rows the run saves. Set 0 for no cap, in which case the run continues until the source runs out. The default is 100 and it applies whether or not the field is touched, so a harvest meant to be larger than that has to raise it or clear it.

## Actor input object example

```json
{
  "query": "coffee",
  "latitude": 37.7749,
  "longitude": -122.4194,
  "span_metres": 20000,
  "area_metres": 0,
  "maxItems": 20
}
```

# Actor output Schema

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

All records scraped by this run

# 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 = {
    "query": "coffee",
    "latitude": 37.7749,
    "longitude": -122.4194,
    "span_metres": 20000,
    "area_metres": 0,
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("thenetaji/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 = {
    "query": "coffee",
    "latitude": 37.7749,
    "longitude": -122.4194,
    "span_metres": 20000,
    "area_metres": 0,
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("thenetaji/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 '{
  "query": "coffee",
  "latitude": 37.7749,
  "longitude": -122.4194,
  "span_metres": 20000,
  "area_metres": 0,
  "maxItems": 20
}' |
apify call thenetaji/google-maps-scraper --silent --output-dataset

```

## MCP server setup

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