# Google Maps Places & Reviews Scraper (`itclan-bd/google-maps-scraper`) Actor

Extract Google Maps business listings and their individual reviews in one run — address, phone, website, categories, price band, opening hours, popular times, rating breakdown, photos and reviews with owner replies, all in the same record. Search many terms and cities at once.

- **URL**: https://apify.com/itclan-bd/google-maps-scraper.md
- **Developed by:** [ITclan BD](https://apify.com/itclan-bd) (community)
- **Stats:** 7 total users, 4 monthly users, 97.1% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 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?

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 Places & Reviews Scraper

**Scrape business listings and their reviews from [Google Maps](https://www.google.com/maps).** Say what to look for and where — search terms with a city, an exact point and radius, Google Maps URLs, or place IDs — and every business comes back as **one dataset record**: name, address, phone, website, categories, rating, price band, coordinates, opening hours, popular times, photos, the star breakdown, and its reviews with owner replies nested inside the same record.

```json
{
  "searchTerms": ["restaurant"],
  "locationQuery": ["Austin, Texas"],
  "maxPlacesPerSearch": 10,
  "maxReviewsPerPlace": 10
}
```

> **This is an unofficial tool and is not affiliated with Google.** See [Is this Actor affiliated with Google?](#is-this-actor-affiliated-with-google)

Use it for lead lists, competitor and market research, review monitoring on a schedule, and location enrichment. Running it on Apify adds scheduling, a REST API, JSON, CSV and Excel exports, and integrations.

### Why use this Google Maps scraper

- **Places and reviews in one run.** Opening hours, popular times, the rating breakdown, photos and reviews arrive in the same record — there is no separate detail step to switch on.
- **Four ways to target what you want** — a location name, an exact circle on the map, Google Maps URLs, or place IDs — combinable in a single run.
- **A stable record shape.** There are 44 fields on every place and 22 fields on every review; a value Google does not publish comes back as `null` or `[]` rather than as a missing column, so CSV headers never shift.
- **Provenance on every row.** `searchTerm`, `searchLocation` and `scrapedAt` say which input produced each record, and when.
- **No headless browser.** Plain HTTP requests at Apify's 512 MB memory floor, which keeps compute usage low.

### Quick start

1. In **Search terms**, type what to look for — `restaurant`, `dentist`. One per line.
2. In **Locations**, type where — `Austin, Texas`. Every term runs in every location.
3. Set **Max places per search term** and **Max reviews per place** (`0` collects business details only, and is much faster).
4. Click **Start**. Results appear on the **Output** tab as they arrive.

The example at the top of this page returns 10 Austin restaurants with 10 reviews each. Terms and locations cross-multiply, so on a bigger job set `maxPlacesTotal` to bound the whole run: three terms in two cities at 100 each can otherwise reach 600 places.

### Ways to target what you scrape

All four can be combined in one run; results are merged and deduplicated on Google's own place identifiers.

#### Search terms with a location name

`searchTerms` + `locationQuery`. The location is a human-readable name; the search is anchored there and widens outward. Both fields are lists, and every term runs in every location.

```json
{
  "searchTerms": ["dentist", "orthodontist"],
  "locationQuery": ["Portland, Oregon", "Boise, Idaho"],
  "maxPlacesPerSearch": 50,
  "maxPlacesTotal": 100
}
```

#### An exact area: coordinates and a radius

`latitude` + `longitude` + `radiusKm`, for when "near Dhaka" is not precise enough — a delivery zone, a catchment area, a sales territory. The radius is a real constraint, not a hint: every result is measured against your point by geographic distance and dropped if it falls outside, the check is repeated against the coordinates that finally ship, and no requests are spent searching ground outside the circle.

```json
{
  "searchTerms": ["restaurant"],
  "latitude": 23.8103,
  "longitude": 90.4125,
  "radiusKm": 5
}
```

A radius needs both coordinates. Next to a location *name* it is ignored with a warning, because a name has no single point to measure from.

#### Exact businesses by place ID

`placeIds`. No search step runs, so nothing else comes back alongside them. These are the same IDs Google's own Places API returns, and the `placeId` of every record this Actor produces, so a previous run's output feeds straight back in.

```json
{
  "placeIds": ["ChIJfbedxbZZwokRpbnR5BBdz1c", "ChIJQXXzJoNbwokRZirX7jSW1i0"],
  "maxReviewsPerPlace": 20
}
```

Unrecognised IDs are skipped with a warning rather than failing the run, and a place named twice — by ID and by URL — is still scraped once.

#### Google Maps URLs

`startUrls` accepts Google Maps links, pasted or uploaded as a file. A `/maps/search/…` link runs that search at the viewport it carries; a `/maps/place/…` link is fetched as **that** business, so a pinned link never returns a nearby branch; a `place_id:ChIJ…` entry — including the `/maps/place/?q=place_id:…` form this Actor emits as `mapsUrl` — names one exact place.

```json
{
  "startUrls": [
    { "url": "https://www.google.com/maps/place/?q=place_id:ChIJQXXzJoNbwokRZirX7jSW1i0" }
  ],
  "maxReviewsPerPlace": 50
}
```

This box takes Google Maps URLs only — it is not a general web crawler, and any other link is skipped with a warning. `?cid=` share links are rejected with a message saying what to paste instead: a cid carries only half of Google's identifier and cannot be resolved back to a place.

### Input

Configure this on the Input tab, or pass JSON through the API.

| Field | Type | Default | What it does |
|---|---|---|---|
| `searchTerms` | array | — | What to look for, one term per line. Every term runs in every location; results are merged and deduplicated, so overlapping terms are safe |
| `locationQuery` | array | — | Where to search. A place name such as `Berlin, Germany`, or a point written as `40.758, -73.9855`. Names and points can be mixed |
| `latitude` | number | — | Search around an exact point. Needs `longitude` too; one on its own is ignored with a warning |
| `longitude` | number | — | The other half of the point, in decimal degrees |
| `radiusKm` | number | — | Hard geographic bound on `latitude`/`longitude`: nothing further away is returned, and ground outside it is not searched. Maximum `500` |
| `startUrls` | array | — | Google Maps URLs. `/maps/search/…` runs a search, `/maps/place/…` scrapes that exact business, `place_id:ChIJ…` names an exact place |
| `placeIds` | array | — | Exact businesses by Google place ID, one per line. No search step runs |
| `maxPlacesPerSearch` | integer | `20` | How many places to collect per search term. Also decides how far the search widens, so a lower number searches a smaller area |
| `maxPlacesTotal` | integer | `0` | Ceiling for the whole run across every term and location. `0` means no limit. Counts unique places, applied before any details are fetched, and shared evenly across your searches so every city is represented |
| `maxReviewsPerPlace` | integer | `0` | Reviews per place. `0` issues no review requests at all. Maximum `500` |
| `reviewsSort` | string | `mostRelevant` | `mostRelevant` is Google's own ordering; `newest` returns the most recently added or edited reviews first. A `reviewsStartDate` overrides this |
| `reviewsStartDate` | string | — | Only reviews added or edited on or after this date. Accepts `2026-08-01`, `2026-08-01T09:30:00Z`, or a span such as `7 days` or `25 hours` |
| `maxImagesPerPlace` | integer | `50` | Photo URLs kept per place. `0` omits them. Maximum `100` |
| `language` | string | `en` | Language for names, categories, addresses and hours — 74 languages. Google localises this server-side, so it changes the data, not just the labels |

**A run needs at least one complete instruction:** a Google Maps URL, a place ID, or a search term paired with a location. Anything half-specified is skipped with a warning, so search terms with no location still let your URLs and place IDs run. The run fails only when nothing at all is runnable.

### Output

One dataset item per place, with its reviews nested inside. Export as JSON, CSV, Excel, HTML or XML from the Output tab or the API.

```json
{
  "name": "ARVACI Coffee",
  "placeId": "ChIJQXXzJoNbwokRZirX7jSW1i0",
  "featureId": "0x89c25b8326f37541:0x2dd69634eed72a66",
  "address": "167 Rogers Ave, Brooklyn, NY 11216, United States",
  "phone": "+1 518-709-4680",
  "phoneUnformatted": "+15187094680",
  "website": null,
  "isClaimed": true,
  "rating": 4.7,
  "reviewCount": 384,
  "reviewsDistribution": { "oneStar": 14, "twoStar": 4, "threeStar": 16, "fourStar": 26, "fiveStar": 324 },
  "categories": ["Coffee shop"],
  "priceRange": "$1–20",
  "coordinates": { "lat": 40.6716473, "lng": -73.9529333 },
  "openingHours": { "monday": "7 AM–5 PM" },
  "openingHoursPeriods": { "monday": [{ "open": "07:00", "close": "17:00" }] },
  "popularTimes": { "monday": [{ "hour": 7, "occupancyPercent": 38 }] },
  "additionalInfo": { "Service options": ["Delivery", "Takeaway", "Dine-in"] },
  "mapsUrl": "https://www.google.com/maps/place/?q=place_id:ChIJQXXzJoNbwokRZirX7jSW1i0",
  "searchTerm": "coffee shop",
  "searchLocation": "Brooklyn, New York",
  "scrapedAt": "2026-09-02T06:28:37Z",
  "responseTruncated": false,
  "reviewsExtractedCount": 1,
  "reviews": [
    {
      "reviewId": "Ci9DQUlRQUNvZENodHljRjlvT2xKYWVGVjZlV00zTUZkSE9FeGxlRUZyUnpSVWFWRRAB",
      "author": "Kara Lechonsito",
      "authorIsLocalGuide": true,
      "rating": 5.0,
      "publishedAtDate": "2026-04-23T04:44:10Z",
      "lastEditedAtDate": "2026-04-23T04:44:10Z",
      "text": "I did not expect much when I ordered my coffee, but the first sip caught my attention.",
      "textTranslated": null,
      "reviewDetailedRating": { "Food": 5.0, "Service": 5.0, "Atmosphere": 5.0 },
      "responseFromOwnerText": "That is exactly what I feel, thank you so much.",
      "images": []
    }
  ]
}
```

#### Place fields

| Group | Fields |
|---|---|
| Identity | `name`, `address`, `street`, `phone`, `phoneUnformatted` (E.164), `website`, `description`, `editorialSummary` |
| Ratings | `rating`, `reviewCount`, `reviewsDistribution` (reviews per star, summing to `reviewCount`) |
| Classification | `categories`, `categoryId` (Google's own key, identical in every language), `priceRange`, `isClaimed` |
| Location | `coordinates`, `plusCode`, `plusCodeGlobal`, `timezone` |
| Hours and traffic | `openingHours`, `openingHoursPeriods`, `popularTimes`, `timeSpent` |
| Rich detail | `additionalInfo` (amenities as Google's About tab groups them), `imageCategories`, `images`, `imagesCount`, `fuelPrices`, `evCharging`, `hotel` |
| Identifiers | `placeId`, `featureId`, `cid`, `kgmid`, `mapsUrl` |
| Reviews | `reviews`, `reviewsExtractedCount`, `reviewsCappedByLimit`, `reviewsStartDateApplied` |
| Run metadata | `searchTerm`, `searchLocation`, `scrapedAt`, `responseTruncated`, `reviewsError` |

Which optional fields are filled depends on the business, not on the run. Names, addresses, categories, coordinates and ratings are effectively always present. `priceRange` is largely food and drink, `popularTimes` needs enough foot traffic for Google to model it, and `fuelPrices`, `hotel`, `evCharging` and `editorialSummary` apply to a minority of listings. An absent value is `null`, not an error.

Three fields deserve a note:

- **`isClaimed`** is three-state: `true` when Google shows the listing has an owner, `false` when Google is offering it to be claimed, `null` when neither signal was present or both were. **`null` means "not known", never "not claimed"** — it is never guessed from absence. Owner identifiers are not published.
- **`images`** holds photos from the place's own Google gallery only. Reviewer avatars, review photos and *other* businesses' photos are excluded — a review's photos sit on that review, the reviewer's avatar in `authorPhotoUrl`. `imagesCount` is the gallery size Google *reports*, usually far larger than the length of `images`, which `maxImagesPerPlace` caps.
- **`openingHoursPeriods`** is the machine-readable form of `openingHours`: 24-hour time, independent of `language`. **A `close` at or before the `open` means the interval runs into the next day.**

| The business | `openingHours` | `openingHoursPeriods` |
|---|---|---|
| Open 9 to 5 | `"9 AM–5 PM"` | `[{"open": "09:00", "close": "17:00"}]` |
| Bar, noon until 2 AM | `"12 PM–2 AM"` | `[{"open": "12:00", "close": "02:00"}]` |
| Open 24 hours | `"Open 24 hours"` | `[{"open": "00:00", "close": "00:00"}]` |
| Closed that day | `"Closed"` | `[]` |

An empty list means no interval *starts* that day — a venue open across midnight may already be covered by the previous day's.

#### Review fields

| Group | Fields |
|---|---|
| The review | `reviewId`, `reviewUrl`, `rating`, `text`, `textTranslated`, `images` |
| Dates | `publishedAtDate`, `lastEditedAtDate` (both ISO-8601 UTC), `timeAgo` |
| Reviewer | `author`, `authorId`, `authorProfileUrl`, `authorPhotoUrl`, `authorReviewCount`, `authorIsLocalGuide` |
| Owner reply | `responseFromOwnerText`, `responseFromOwnerDate`, `responseFromOwnerEditedDate` |
| Structured extras | `reviewDetailedRating` (per-aspect scores), `reviewContext` (guided visit answers), `originalLanguage`, `translatedLanguage` |

**`text` is the review as its author wrote it**, in the original language whatever language the run asked for. When Google machine-translated it for display, that translation is in **`textTranslated`**, with the two languages named in `originalLanguage` and `translatedLanguage`. The translation is never appended to `text`. Where the review is already in the run's language, `textTranslated` is `null`.

#### Three views on the same data

| View | One row per | Use it for |
|---|---|---|
| Overview | place | the fields most runs are about |
| Reviews (one per row) | review | review analysis, sentiment, CSV and Excel exports |
| All fields | place | everything, including identifiers and diagnostics |

Pick one on the Output tab, or add `&view=reviews` to the dataset URL:

```
https://api.apify.com/v2/datasets/{DATASET_ID}/items?clean=true&format=csv&view=reviews
```

That flattens the nested `reviews` array so each review is its own row with its business repeated alongside. In that view `rating` and `images` belong to the **review**; the business total stays in `reviewCount`.

> **If you also narrow the columns with `&fields=`, that list must include `reviews`.** Apify applies `fields` before the unwind, so leaving it out silently returns one row per business — no error, and the symptom is a row with no `reviewId`. The same applies to `&unwind=reviews`.
>
> ```
> # wrong - ...items?view=reviews&fields=name,reviewId,author,rating
> # right - ...items?view=reviews&fields=name,reviews,reviewId,author,rating
> ```

### Reviews

Set `maxReviewsPerPlace` above `0` and reviews arrive inside the same record as the business. `reviewsSort` picks the order: `mostRelevant` is what the place's own Google Maps page shows, `newest` puts the most recently added or edited first.

#### Monitoring new and edited reviews

Set `reviewsStartDate` and each place returns only the reviews added or edited since then, newest first. The Actor stops as soon as it reaches older reviews rather than paging the whole list, which is what makes a daily or weekly monitoring run cheap.

```json
{
  "placeIds": ["ChIJfbedxbZZwokRpbnR5BBdz1c"],
  "maxReviewsPerPlace": 100,
  "reviewsStartDate": "25 hours"
}
```

Three things to know about the date filter:

- **A start date always sorts newest-first**, overriding `reviewsSort`: stopping early depends on the list being ordered by last-edited time, and no other ordering provides that. The log says so when it happens.
- **The comparison uses `lastEditedAtDate`, not `publishedAtDate`** — that is the order Google returns reviews in, and the only field a run can safely stop on. A review written years ago but edited yesterday **is** returned, which is usually what you want. Both dates are on every review, so filter on `publishedAtDate` in your export if you only want newly written ones.
- **The date decides which reviews qualify; `maxReviewsPerPlace` decides how many come back.** The cap wins: when more qualify than it allows you get the newest, flagged `reviewsCappedByLimit: true`. Each row also carries `reviewsStartDateApplied`, the UTC instant a relative span resolved to.

On a daily schedule, `25 hours` rather than `24` overlaps the previous run so a review arriving mid-run is not missed; deduplicate on `reviewId` and the overlap costs nothing.

### Limits and known behaviour

- **A single search point returns roughly 170 results.** That is Google's behaviour, not a setting. Ask for more and the Actor widens outward across neighbouring areas, stopping after 60 search centres per term or once it stops finding anything new. Extra search terms usually find more businesses over the same ground than a bigger number does.
- **How much ground one point covers depends on how dense the area is.** One `restaurant` query reached 0.52 km in Manhattan, 2.10 km in Berlin Mitte, 3.30 km in suburban Plano, 18.05 km on Maui and 101.42 km in rural Bangladesh. Very large regions are better split by city or category than asked for in one run.
- **Results stay near where you asked.** When an area has no matches, Google can answer with businesses from an unrelated region rather than an empty page. Results further than 500 km from the requested point are discarded, so an empty area returns nothing instead of somewhere else's businesses. With `radiusKm` set, your radius applies instead.
- **Very large runs can lose three detail fields.** An incomplete response is retried, and if it is still incomplete the row is flagged `responseTruncated: true` rather than dropped. Such a row keeps name, address, coordinates, identifiers, categories, rating, phone, website and hours, and loses only `description`, `priceRange` and `popularTimes`. It is a property of the run, not of the place — the same places re-fetched alone come back complete. Runs of a few hundred places flag nothing; it appears on runs of many hundreds, so split those by city or category if these three fields matter.
- **Up to 500 reviews per place per run.** Google also stops issuing review pages for some places before their stated `reviewCount` is reached, so a place can legitimately return fewer reviews than it claims to have.
- **Up to 100 photos per place**, 50 by default. Most places carry far fewer, so a short list is normal rather than a problem.
- **Optional fields are absent for some places**, depending on category, country and what Google publishes. Missing values stay `null`; nothing is inferred from another source.
- **No email enrichment.** It returns the website a business lists, but it does not visit that website and does not extract email addresses.
- **There is no proxy setting.** Google ties a session to the IP that obtained its cookies, so a rotating exit IP causes silently incomplete responses. Every run uses the configuration measured to return the most complete data.
- **Runtime is not deterministic.** Google paces and varies its responses, so two identical runs can differ noticeably in how long they take.

A place that fails does not fail the run: the row arrives flagged with `responseTruncated`, `reviewsError` or `reviewsCappedByLimit`, and a completeness summary is written to the run log.

### Tips

- **For review work use the Reviews view** (`&view=reviews`) — exporting the place view to CSV puts the whole review array into one cell.
- **To find unanswered reviews**, export that view and filter on an empty `responseFromOwnerText`.
- **For recurring monitoring**, discover the businesses once, keep their `placeId` values, then re-run against those IDs with `reviewsStartDate` set slightly longer than the schedule interval.
- **Set `maxReviewsPerPlace: 0`** when you only need business details — the single largest reduction in work a run can make.
- **Set `maxImagesPerPlace: 0`** if you do not need photos. They cost no extra requests either way, but they are a large share of a listings-only record, so dropping them makes exports much smaller.
- **Split a multi-city run** back into per-city lists using `searchTerm` and `searchLocation`.

### FAQ

#### Can I scrape several cities in one run?

Yes. Search terms and locations are both lists, every term runs in every location, and results are deduplicated on Google's place identifiers. Use `maxPlacesTotal` to bound the total, and see [Output](#output) for what each row contains.

#### Can I scrape one specific business, or re-scrape a known list?

Yes. Paste a `/maps/place/…` URL into **Google Maps URLs**, or the `placeId` into **Place IDs** — both route to that exact business, never a nearby branch. Since `placeId` and `mapsUrl` are on every record, a previous run's output feeds straight back in.

#### Can I scrape a whole city or region?

Within limits — see [Limits and known behaviour](#limits-and-known-behaviour). Adding search terms is usually more effective than raising the number, and very large areas are best split into several runs.

#### Why did an old review appear in a recent-review run?

Because it was edited inside your window. The filter compares `lastEditedAtDate`, which is the order Google returns reviews in. Filter on `publishedAtDate` in your export if you only want newly written ones.

#### Why do some places return fewer reviews than their review count?

Google stops issuing review pages for some places. That is Google's behaviour rather than a limit of this Actor; `reviewsExtractedCount` always says how many were collected.

#### Does it collect email addresses, and do I need a proxy?

No to both. It returns what Google publishes, including the business website when one is listed, but it does not visit that website. There is no proxy option to configure.

#### Is this Actor affiliated with Google?

No. This is an **unofficial** tool. It is not affiliated with, sponsored by, endorsed by or connected to Google LLC or Google Maps in any way, and it is not built on the Google Maps Platform or any Google API. "Google" and "Google Maps" are marks of Google LLC, used here only to describe where the data comes from.

### Data source and personal data

All data comes from Google Maps' own public listings, in the language you select. Nothing is inferred or enriched from another source: where Google publishes no value, the field is `null` or empty.

### Support

Found a problem, or need something this Actor does not do? Open an issue on the **Issues** tab of this Actor's Apify page.

# Actor input Schema

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

What to look for, one term per line — for example <code>dentist</code>, <code>orthodontist</code>. Every term is searched in every location, and the results are merged and deduplicated, so overlapping terms are safe and are a good way to find more businesses in one area.<br><br>Needs at least one location below.

## `locationQuery` (type: `array`):

Where to search, one per line. Either a place name — <code>Berlin, Germany</code>, <code>Austin, Texas</code> — or an exact point as <code>latitude, longitude</code>, for example <code>40.758, -73.9855</code>. Names and points can be mixed freely in the same list.<br><br>Every search term is run against every location, so three terms across two locations is six searches.

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

Search around an exact point instead of a place name. Give <b>both</b> latitude and longitude — one on its own is ignored with a warning.<br><br>Decimal degrees, -90 to 90. Use this with <b>Search radius</b> below when you need a defined area rather than "near this city".

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

The other half of the point. Decimal degrees, -180 to 180.

## `radiusKm` (type: `number`):

A <b>hard</b> limit on how far from the point above a business may be. Nothing outside it is returned: every result is measured against the point by real geographic distance and dropped if it is further away, and the Actor does not spend requests searching ground outside the circle either.<br><br>Applies only to a latitude and longitude. A location given by name has no single point to measure from, so a radius set without coordinates is ignored with a warning. Leave it empty to let the search decide how far to widen on its own.

## `startUrls` (type: `array`):

Google Maps links, pasted or uploaded as a file. This is not a general web crawler — only Google Maps URLs are accepted, and each is read for what it names:<br><br>• a <code>/maps/search/…</code> link runs that search, at the viewport the link carries;<br>• a <code>/maps/place/…</code> link is fetched as <b>that</b> business, so a pinned link never returns a different branch;<br>• a <code>place\_id:ChIJ…</code> entry — including the <code>/maps/place/?q=place\_id:…</code> form this Actor emits as <code>mapsUrl</code> — is an exact place, the same as putting it in <b>Place IDs</b> below.<br><br>Can be used on its own or combined with the search above; everything is merged and deduplicated. <code>?cid=</code> share links are still rejected with a clear error: a cid carries only half of Google's internal id and cannot be resolved back to a place.

## `placeIds` (type: `array`):

Scrape exact businesses you already know, one Google place id per line — <code>ChIJfbedxbZZwokRpbnR5BBdz1c</code>. No search step is run, so nothing else comes back with them.<br><br>These are the ids Google's own Places API returns, and the <code>placeId</code> field of every result this Actor produces, so a previous run's output can be fed straight back in for re-scraping or monitoring. Ids that Google does not recognise are reported and skipped rather than failing the run, and a place named twice — by id and by URL, say — is still scraped once.

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

How many places to collect for each search term. Google answers a single point with roughly 170 results at most, so beyond that the Actor automatically widens the search across neighbouring areas until it reaches your number or runs out of businesses.<br><br>This shapes the search itself, not just how much is kept: a lower number also searches a smaller area.

## `maxPlacesTotal` (type: `integer`):

A hard ceiling on the run, across every search term and location combined. <code>0</code> means no limit. Useful because the setting above applies to <b>each</b> search: three terms in two cities at 100 each can reach 600 places.<br><br>It counts <b>unique</b> places and is applied before any business details are fetched, so it bounds what the run costs. The cap is shared evenly across your searches, so every term and city is represented rather than the first ones using up the whole budget. Places named directly by a Google Maps URL are always kept first.

## `maxReviewsPerPlace` (type: `integer`):

Reviews to collect for each place, up to 500. <b>0 makes no review requests at all</b> — leave it at 0 if you only need business details, since that is the largest single reduction in work a run can make.

## `reviewsSort` (type: `string`):

<b>Most relevant</b> is Google's own ordering — the same reviews, in the same order, that the place's Google Maps page shows. <b>Newest</b> returns the most recently added or edited reviews first.<br><br><b>A review start date takes precedence over this setting.</b> Set one below and collection switches to newest-first whatever is chosen here, because that is the only order in which the Actor can stop as soon as it reaches reviews older than the date. You are told in the log when that happens.

## `reviewsStartDate` (type: `string`):

Leave empty to collect reviews in the order chosen above. Set a date and each place returns <b>only the reviews added or edited on or after it</b>, newest first — which is what makes a daily or weekly monitoring run cheap: the Actor stops as soon as it reaches older reviews instead of paging through everything.<br><br>The comparison uses a review's <b>last edited</b> time, not its original publication date, because that is the order Google returns reviews in. So a review written in 2019 and edited yesterday <b>is</b> included — which is usually what you want, since an edited review is new information. Every review carries both <code>publishedAtDate</code> and <code>lastEditedAtDate</code> if you need to tell them apart.<br><br>Dates with no time zone are read as UTC. Relative spans are counted back from the moment the run starts, so <code>25 hours</code> on a daily schedule gives you an hour of overlap and never misses a review that arrived mid-run. Needs <b>Max reviews per place</b> above 0, which also caps how many qualifying reviews come back.

## `maxImagesPerPlace` (type: `integer`):

Photo URLs to keep per place. Photos are about <b>half of a record that has no reviews</b> (median 52% across captured places), so lowering this makes a listings-only export markedly smaller. Once reviews are switched on they dominate instead and photos are a much smaller share. Set <code>0</code> if you do not need photos at all.<br><br>These are photos of the business. Reviewer profile pictures are not included here — each review carries its author's photo separately. Most places carry far fewer than the maximum (a median of 10 across captured places), so a lower count is normal rather than a sign of a problem, and no value here costs any extra requests.

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

Google returns names, categories, addresses and opening hours already translated, so this changes the data itself — not just labels.

## Actor input object example

```json
{
  "searchTerms": [
    "restaurant"
  ],
  "locationQuery": [
    "New York, USA"
  ],
  "maxPlacesPerSearch": 20,
  "maxPlacesTotal": 0,
  "maxReviewsPerPlace": 10,
  "reviewsSort": "mostRelevant",
  "maxImagesPerPlace": 50,
  "language": "en"
}
```

# Actor output Schema

## `places` (type: `string`):

No description

## `rawResponses` (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": [
        "restaurant"
    ],
    "locationQuery": [
        "New York, USA"
    ],
    "maxPlacesPerSearch": 20,
    "maxPlacesTotal": 0,
    "maxReviewsPerPlace": 10,
    "maxImagesPerPlace": 50,
    "language": "en"
};

// Run the Actor and wait for it to finish
const run = await client.actor("itclan-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": ["restaurant"],
    "locationQuery": ["New York, USA"],
    "maxPlacesPerSearch": 20,
    "maxPlacesTotal": 0,
    "maxReviewsPerPlace": 10,
    "maxImagesPerPlace": 50,
    "language": "en",
}

# Run the Actor and wait for it to finish
run = client.actor("itclan-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": [
    "restaurant"
  ],
  "locationQuery": [
    "New York, USA"
  ],
  "maxPlacesPerSearch": 20,
  "maxPlacesTotal": 0,
  "maxReviewsPerPlace": 10,
  "maxImagesPerPlace": 50,
  "language": "en"
}' |
apify call itclan-bd/google-maps-scraper --silent --output-dataset

```

## MCP server setup

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