# Google Maps Chain Location Grouper (`zinin/google-maps-chain-location-grouper`) Actor

Group Google Maps locations by observed hostname and compatible brand. Preserve each address and show the evidence for each location assessment, without claiming verified corporate ownership.

- **URL**: https://apify.com/zinin/google-maps-chain-location-grouper.md
- **Developed by:** [Tim Zinin](https://apify.com/zinin) (community)
- **Categories:** Business, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.50 / 1,000 location assessments

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?

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 Chain Location Grouper — link likely chain branches in an existing Maps export by hostname and brand

Feed it a Google Maps places batch you already scraped and get back one location assessment per unique place — an observed hostname+brand correlation flagged for your account-planning verification queue, never a confirmed chain or a legal-ownership claim.

![Google Maps Chain Location Grouper — hero diagram](https://raw.githubusercontent.com/TimmyZinin/apify-actor-assets/d94b27ab62fe5f65d0258277507ea8d341761c8d/value-add-20260923/google-maps-chain-location-grouper/readme-hero.webp)

This Actor does not scrape Google Maps. It reads places already extracted by a Maps scraper — most naturally [Google Maps Scraper](https://apify.com/compass/crawler-google-places) or [Google Maps Extractor](https://apify.com/compass/google-maps-extractor) — via Dataset, run, or inline items, and links likely chain branches by an exact, non-shared corporate hostname plus a compatible normalized brand name. It is a B2B account-planning research tool for teams working from a multi-city batch they already collected — it is not a corporate-ownership verifier, a franchise-registry lookup, or a legal-entity resolver.

### What you get

For each distinct place found across your source batch, you get one location assessment record containing:

- **`entityId`**, **`placeId`**, **`cid`** — the place's stable identity, bounded and type-validated.
- **`hostname`**, **`normalizedBrand`** — the corporate hostname extracted from `website` (or `null` if missing/blank/invalid/excluded), and the brand name normalized for matching (legal suffixes like Inc/LLC/Ltd stripped, punctuation collapsed).
- **`status`**, **`relationship`**, **`groupKey`**, **`groupSize`**, **`groupMembers`** — `standalone`, `shared_brand_hostname_group`, or `excluded_platform`, the matching relationship label, a stable group key, the true group size, and up to 50 lightweight cross-reference entries for other members of the same group.
- **`action`** — `add_to_verification_queue` for a group match, `no_action_standalone` for a unique hostname+brand, or `no_action_excluded_platform` for a known shared-platform host.
- **`confidence`** — a fixed, conservative heuristic (0.65 for an observed group match; 0.4–1 for a standalone, depending on why grouping wasn't possible) — never a calibrated probability of actual corporate ownership.
- **`evidence`** — up to 50 entries, each citing the literal source field, the exact rule that fired, and the source row it came from.
- **`duplicateSourceRowCount`**, **`partial`** — how many source rows shared this exact `placeId`, and a composite disclosure flag for any conflict, truncation, or unresolved signal.
- **Honest diagnostics for everything else** — a row with no `placeId`/`cid`, an oversized identity, or a place with no usable hostname AND no usable, non-generic brand, are all free rows.

What this Actor is explicitly **not**: no claim of verified corporate ownership, no franchise-registry confirmation, no legal-entity resolution. A `shared_brand_hostname_group` match is an OBSERVED, batch-local correlation only — the shared hostname might simply be an uncatalogued generic hosting domain outside this Actor's finite exclusion list. Distinct branches are always preserved as separate locations with their own address, even when linked into the same group.

### Who uses it

- **B2B account planners and franchise-development teams** consolidating a new multi-city Maps export into research-ready account groups, instead of manually cross-referencing addresses.
- **Sales and territory-mapping teams** who need a defensible reason two listings were grouped, rather than an opaque "these might be related" guess.
- **Data-quality teams** auditing an upstream Maps scrape, using resolution states as a signal for how clean the underlying data is.
- **Competitive-intelligence researchers** mapping a competitor's multi-location footprint from a batch of Maps listings they already collected.

Not a scraper: point it at an existing places Dataset from an upstream Actor, most naturally [Google Maps Scraper](https://apify.com/compass/crawler-google-places) (`users30d=37173`, `successfulRuns30d=3,751,440`) or [Google Maps Extractor](https://apify.com/compass/google-maps-extractor) (`users30d=4575`, `successfulRuns30d=327,497`). This Actor never starts either one, and never fetches a page from the open web.

### How to run

1. **Console (fastest for a one-off run).** Open the Actor, go to Input, paste rows into **Source records (inline)** or fill in **Source Dataset**, leave defaults, hit **Start**.
2. **From an existing Maps Dataset.** Put its Dataset ID in `datasetId` — read with your own run's permissions, no separate authorization step.
3. **Chained via Integrations.** Add this Actor as an "Actor/task run" integration on your upstream Maps-scraper Actor; map `{{resource.defaultDatasetId}}` into this Actor's `datasetId` field explicitly (see **Integration recipes**) to *request* a `READ` grant — a bare Dataset ID or webhook payload alone only requests access, it never guarantees it.
4. **Via the API.** Call `POST /v2/acts/6OruMc0vPsndzCW0s/runs` (or `run-sync-get-dataset-items`) with `datasetId` (or `items`) and, for a spending ceiling, the run option `maxTotalChargeUsd`.
5. **Tune the matching strictness.** `brandCompatibilityMode` (`prefix` default or `exact`) controls how strictly normalized brand names must match; `additionalExcludedHostnames` adds your own shared-platform exclusions on top of the built-in list.
6. **Read the results.** One row per unique place lands in the run's default Dataset, with a matching `OUTPUT`/`SUMMARY` record describing delivery, billing, and coverage.

Every run is independent processing — see **Operating guide** for what that means for re-running against the same batch.

### Pricing

Pay-per-event, one event type: **`result-found`** ("Location assessment" in the platform's pricing UI — the primary pricing noun used throughout this listing, matching this product's own `noun` field) — one delivered location assessment (one unique place). There is **no start fee**, and every free diagnostic (a missing identity, an unresolved relationship, a budget stop, an empty batch) requests no charge event at all.

| Tier | Price per location assessment | Discount |
|---|---|---|
| Free (default) | $0.015 | — |
| Bronze | $0.0135 | 10% |
| Silver | $0.012 | 20% |
| Gold | $0.0105 | 30% |
| Platinum | $0.0105 | 30% |
| Diamond | $0.0105 | 30% |

These are Apify's platform-wide account pricing tiers — not a volume schedule this Actor invents — assigned automatically by your account's usage; you don't select one. One location assessment is one unique place across the whole batch, never billed per duplicate source row.

**A note on the evidence in this README:** every output example below comes from the product owner's own verification runs against this Actor's accepted build — cost to the owner, not sales or customer revenue.

Set the platform's own **Max total charge (USD)** run option to cap spend. If testing the zero-paid path via a raw API call, do not rely on literal `maxTotalChargeUsd: 0` — the platform's own API-level cap-normalization behavior means a positive value below every tier's price only reliably guarantees zero paid results when the effective cap the run actually enforces is preserved at that value — see **Evidence and boundaries** for the real recorded run demonstrating this.

### Input contract

| Field | Type | Default | Notes |
|---|---|---|---|
| `items` | array (JSON editor), max 1000 entries | prefilled real example | Inline source records. Ignored if `datasetId`, `runId`, or a resolvable `payload.resource` is also given. |
| `datasetId` | string (Dataset picker, READ) | — | Dataset ID from a Maps-scraper run. Read with your own run's permissions. Overrides inline `items`. |
| `runId` | string, max 64 chars | — | An Actor run ID to read instead of a Dataset ID; its default Dataset resolves under your own permissions — no upstream Actor started. |
| `payload` | object (JSON editor) | — | Optional `ACTOR.RUN.SUCCEEDED`-shaped payload carrying `resource.defaultDatasetId`/`resource.id`. The `datasetId` resource picker is the recommended way to request access; a bare payload reference alone does not grant it. |
| `maxInputRows` | integer | 100 | Hard cap on source records examined before grouping (1–1000). Still bounded to 4 MiB total / 64 KiB per row. |
| `maxResults` | integer | 10 | Max useful results this run can deliver (1–1000), independent of the platform's Max total charge option. |
| `brandCompatibilityMode` | string (`prefix`/`exact`) | `prefix` | `prefix` allows "Metro Cafe" and "Metro Cafe Express" to link (every whole word of the shorter normalized brand matches the longer one, in order); `exact` requires identical strings. Hostname match is always required either way — brand alone never links locations. |
| `additionalExcludedHostnames` | array of bare hostnames | `[]` | Extra shared-platform hostnames to exclude, on top of the built-in list. Each entry must be a bare hostname — no scheme/path/port/userinfo/whitespace; an invalid entry rejects the whole batch as `invalid_domain_options`, never silently ignored. |

**Source precedence, exactly:** an explicit `datasetId` or `payload.resource.defaultDatasetId` (they must agree if both are present) beats a `runId` or `payload.resource.id`, which beats inline `items`. A dataset/run source is never merged with inline rows. A malformed or conflicting reference is rejected as invalid input, never silently downgraded to the inline example. Private-Dataset access is conditional: the resource picker or an authorized Integrations mapping can *request* a `READ` grant, but neither one *guarantees* the platform actually honors it — a bare Dataset ID string or webhook reference alone never grants access to data you aren't already permitted to read.

**Accepted row fields** (any subset present is used; nothing is strictly required except a place identity):

| Field | Role |
|---|---|
| `placeId` / `cid` | Primary entity identity. `placeId` (string, ≤500 chars) is preferred; `cid` (string or finite number) is used when `placeId` is absent. Over the bound, the row is rejected outright (`oversized_identity`), never truncated. |
| `website` | Resolved to `dataset_missing` (key absent), `observed_absent` (explicit blank), `invalid_url` (unsupported scheme, malformed URL, or userinfo present), `excluded_platform` (a known shared booking/social/website-builder host or subdomain), or `usable` (a real hostname extracted, ≤253 chars). |
| `title` | Normalized into a matching brand key: lowercased, punctuation stripped, legal suffixes (`inc`/`llc`/`ltd`/`co`/`corp`/`company`/`group`) removed. Over 500 characters is rejected outright as unresolved — never truncated, since truncating a matching key risks merging two genuinely different brands sharing a long common prefix. A brand normalizing to only generic words (`company`/`store`/`shop`/`business`/`group`/`market`/`services`/`shops`/`stores`, articles ignored) is never usable clustering evidence either. |
| `phone`, `address`, `city`, `countryCode`, `categoryName` | Pass-through display fields, each bounded and disclosed if truncated or malformed; none is a chain-linking signal on its own — a shared phone number alone never collapses two locations into one group. |
| `scrapedAt` | Picks the canonical occurrence when duplicate rows share the same identity — most recent valid timestamp wins, else the last occurrence deterministically. |
| `found`, `failed`, `status`, `error`, `errors` | An explicit hard-failure signal is always a free `source_reported_failure` diagnostic, even with `found: true`/`partial: true` also present. A non-empty `error`/`errors` alone is a harmless warning ONLY when the same row also explicitly carries `found: true` **and** `partial: true`; otherwise it still produces the free diagnostic. |

**Hard read bounds, always enforced regardless of `maxInputRows`:** at most 1000 source rows, at most 4 MiB of total source JSON per run, at most 64 KiB per individual row (an oversized row is dropped and disclosed). This Actor reads only the fixed `https://api.apify.com` origin with your run's own token — it never fetches an arbitrary URL and never starts an upstream Actor.

### Output examples

Every example below is a real record from an actual accepted run of this Actor's build `0.1.2` (Actor ID `6OruMc0vPsndzCW0s`, build `f5ZhSXhcljvptI1pa`), reproduced exactly — nothing is hand-edited. Source dates come from `scrapedAt` (2026-09-22T23:07:38–42Z); the runs were processed 2026-09-23 — a saved snapshot, not a live re-check. This real 3-place London-museum batch happens to have three distinct, non-shared hostnames — a genuine, honest `standalone` outcome for each, not a fabricated grouped example.

#### 1. First useful assessment — a full, real dataset row

This run used the Actor's own real inline prefill: 3 real London museum listings. Both diagrams on this page illustrate this exact run — the same 3 real places, all delivered as paid results.

Actor input (first of three real prefill rows; abbreviated to the fields this Actor reads):

```json
{
  "title": "Natural History Museum",
  "categoryName": "Natural history museum",
  "address": "Cromwell Rd, South Kensington, London SW7 5BD, United Kingdom",
  "city": "London",
  "countryCode": "GB",
  "website": "https://www.nhm.ac.uk/",
  "phone": "+44 20 7942 5000",
  "placeId": "ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
  "cid": "8914844151054820548",
  "scrapedAt": "2026-09-22T23:07:38.397Z",
  "_exampleProvenance": {
    "sourceActorId": "nwua9Gu5YrADL7ZDj",
    "sourceRunId": "5VeVFvxu0MQhPBrWY",
    "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
    "curatedPublicSnapshot": true,
    "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities."
  }
}
```

First Dataset output row (of three delivered from this run):

```json
{
  "entityId": "ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
  "found": true,
  "status": "standalone",
  "action": "no_action_standalone",
  "confidence": 1,
  "sourceRowCount": 1,
  "placeId": "ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
  "cid": "8914844151054820548",
  "title": "Natural History Museum",
  "titleTruncated": false,
  "website": "https://www.nhm.ac.uk/",
  "websiteTruncated": false,
  "phone": "+44 20 7942 5000",
  "phoneTruncated": false,
  "address": "Cromwell Rd, South Kensington, London SW7 5BD, United Kingdom",
  "addressTruncated": false,
  "city": "London",
  "cityTruncated": false,
  "countryCode": "GB",
  "countryCodeTruncated": false,
  "categoryName": "Natural history museum",
  "categoryNameTruncated": false,
  "hostname": "nhm.ac.uk",
  "normalizedBrand": "natural history museum",
  "groupKey": "standalone:ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
  "groupSize": 1,
  "relationship": "standalone",
  "groupMembers": [],
  "groupMembersTruncated": false,
  "duplicateSourceRowCount": 1,
  "observedAt": "2026-09-22T23:07:38.397Z",
  "sourceObservationWindow": { "earliest": "2026-09-22T23:07:38.397Z", "latest": "2026-09-22T23:07:38.397Z", "validCount": 1, "totalRowCount": 1 },
  "evidence": [
    { "field": "website", "value": "nhm.ac.uk", "rule": "no_peer_in_batch: hostname \"nhm.ac.uk\" resolved but no other distinct place id in the provided dataset shares it; this reflects the provided batch only, not an independent company-ownership verification", "sourceRowIndex": 0 }
  ],
  "businessPartial": false,
  "sourceRowIndexes": [0],
  "partial": false,
  "sourceCoverage": { "complete": true, "reason": null, "droppedOversized": 0 },
  "product": "google-maps-chain-location-grouper",
  "schemaVersion": "1.0",
  "sourceDatasetId": null,
  "sourceRunId": null,
  "suppliedSourceRunId": null,
  "sourceRunIdVerified": null,
  "processedAt": "2026-09-23T05:39:27.922Z"
}
```

`observedAt`/`sourceObservationWindow` reflect the sole contributing occurrence's real, whitespace-trimmed `scrapedAt` — `earliest`/`latest` are identical here because only one row contributed.

`hostname: "nhm.ac.uk"` resolved from the website, and `normalizedBrand: "natural history museum"` from the title. Because no other place in this 3-row batch shares that exact hostname, the result is `standalone` — a real, paid conclusion, not an unresolved diagnostic and not a false grouping claim.

#### 2. Derived partial-flag control — same real values, an explicit incompleteness flag injected

This is **not** a demonstration of removed columns or actual upstream data incompleteness. It is the identical real discovery batch as example 1, with each source row's own `partial` field deliberately set to `true` (and `found: true` added), to exercise this Actor's disclosed-incompleteness path under a controlled, known input. Every underlying value is identical and real; only the incompleteness flag is a deliberate test control, not an observed condition from any actual scrape.

Actor input (identical to example 1, except `"found": true, "partial": true` added):

```json
{
  "title": "Natural History Museum",
  "categoryName": "Natural history museum",
  "address": "Cromwell Rd, South Kensington, London SW7 5BD, United Kingdom",
  "city": "London",
  "countryCode": "GB",
  "website": "https://www.nhm.ac.uk/",
  "phone": "+44 20 7942 5000",
  "placeId": "ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
  "cid": "8914844151054820548",
  "scrapedAt": "2026-09-22T23:07:38.397Z",
  "_exampleProvenance": {
    "sourceActorId": "nwua9Gu5YrADL7ZDj",
    "sourceRunId": "5VeVFvxu0MQhPBrWY",
    "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
    "curatedPublicSnapshot": true,
    "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities."
  },
  "found": true,
  "partial": true
}
```

Resulting row (`businessPartial: true`, `partial: true` — otherwise identical to example 1's row):

```json
{
  "entityId": "ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
  "found": true,
  "status": "standalone",
  "action": "no_action_standalone",
  "confidence": 1,
  "sourceRowCount": 1,
  "placeId": "ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
  "cid": "8914844151054820548",
  "title": "Natural History Museum",
  "titleTruncated": false,
  "website": "https://www.nhm.ac.uk/",
  "websiteTruncated": false,
  "phone": "+44 20 7942 5000",
  "phoneTruncated": false,
  "address": "Cromwell Rd, South Kensington, London SW7 5BD, United Kingdom",
  "addressTruncated": false,
  "city": "London",
  "cityTruncated": false,
  "countryCode": "GB",
  "countryCodeTruncated": false,
  "categoryName": "Natural history museum",
  "categoryNameTruncated": false,
  "hostname": "nhm.ac.uk",
  "normalizedBrand": "natural history museum",
  "groupKey": "standalone:ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
  "groupSize": 1,
  "relationship": "standalone",
  "groupMembers": [],
  "groupMembersTruncated": false,
  "duplicateSourceRowCount": 1,
  "observedAt": "2026-09-22T23:07:38.397Z",
  "sourceObservationWindow": { "earliest": "2026-09-22T23:07:38.397Z", "latest": "2026-09-22T23:07:38.397Z", "validCount": 1, "totalRowCount": 1 },
  "evidence": [
    { "field": "website", "value": "nhm.ac.uk", "rule": "no_peer_in_batch: hostname \"nhm.ac.uk\" resolved but no other distinct place id in the provided dataset shares it; this reflects the provided batch only, not an independent company-ownership verification", "sourceRowIndex": 0 }
  ],
  "businessPartial": true,
  "sourceRowIndexes": [0],
  "partial": true,
  "sourceCoverage": { "complete": true, "reason": null, "droppedOversized": 0 },
  "product": "google-maps-chain-location-grouper",
  "schemaVersion": "1.0",
  "sourceDatasetId": null,
  "sourceRunId": null,
  "suppliedSourceRunId": null,
  "sourceRunIdVerified": null,
  "processedAt": "2026-09-23T05:39:45.900Z"
}
```

Notice the assessment itself is unchanged from example 1; only `businessPartial`/`partial` flip to `true` because the source row disclosed its own incompleteness. This "partial-columns" fixture is a historical folder-name label only: the actual test retains every real value and injects `found:true`/`partial:true` as a controlled flag, never removing or blanking a real column.

#### 3. Below-price run — free `budget_stopped` diagnostic, no partial charge

```json
[
  {
    "product": "google-maps-chain-location-grouper",
    "schemaVersion": "1.0",
    "status": "budget_stopped",
    "reason": "Run charge limit cannot cover any result; no source was read.",
    "found": false,
    "processedAt": "2026-09-23T05:39:57.821Z"
  }
]
```

This run used the platform run option `maxTotalChargeUsd: 0.005` — below every tier's per-assessment price (even Gold/Platinum/Diamond's $0.0105). The Actor never reads the source batch, delivers zero paid assessments, and writes this one free diagnostic. See **Evidence and boundaries** for the API-level zero-cap caveat.

#### 4. Empty batch — free `no_results` diagnostic

Actor input:

```json
{ "items": [], "maxInputRows": 100, "maxResults": 10 }
```

Dataset output:

```json
[
  {
    "product": "google-maps-chain-location-grouper",
    "schemaVersion": "1.0",
    "status": "no_results",
    "found": false,
    "reason": "no_usable_rows_in_batch",
    "sourceCoverage": { "complete": true, "reason": null, "droppedOversized": 0 },
    "processedAt": "2026-09-23T05:40:05.619Z"
  }
]
```

An empty or entirely-unusable batch is a free diagnostic, never a silent zero-row success and never a charge.

### Field dictionary

**Per-assessment result fields:**

| Field | Type | Meaning |
|---|---|---|
| `entityId` | string | `placeId`, or `cid:<cid>` when `placeId` is absent — stable key for this place. |
| `found` | boolean | `true` on every billed assessment; `false` on every free diagnostic. |
| `status` | string | `standalone`, `shared_brand_hostname_group`, or `excluded_platform` on an assessment; a diagnostic reason (`invalid_row`, `source_reported_failure`, `missing_identity`, `oversized_identity`, `insufficient_source_data`, `unresolved_relationship`, `invalid_domain_options`, `budget_stopped`, `no_results`, `max_results_reached`) on a free row. |
| `relationship` | string | `standalone`, `observed_brand_host_match`, or `excluded_platform`. `observed_brand_host_match` is an OBSERVED batch-local correlation only (fixed 0.65 confidence) — never a confirmed chain/ownership claim. |
| `action` | string | `no_action_standalone`, `add_to_verification_queue`, or `no_action_excluded_platform`. |
| `placeId` / `cid` | string/number, null | Bounded, type-validated identity fields; a malformed non-primary identity is nulled rather than echoed verbatim. |
| `hostname` | string, null | Corporate hostname extracted from `website` (lowercased, `www.` stripped), or `null` if missing/blank/invalid/excluded. |
| `normalizedBrand` | string, null | Brand key derived from `title`, or `null` if unresolved/oversized. |
| `groupKey` | string | `standalone:<entityId>`; `hostbrand:<hostname>\|<brand>` for a linked group; `excluded_platform:<entityId>`. |
| `groupSize` | number | True count of distinct place ids in this group (1 for standalone/excluded). |
| `groupMembers` | array | Up to 50 other locations in the group, each `{ placeId, title, address }` only — bounded far tighter (80/120 chars) than the primary row's own fields. |
| `groupMembersTruncated` | boolean | `true` when the group has more than 50 other members; `groupSize` still reflects the true total. |
| `title`/`titleTruncated`, `website`/`websiteTruncated`, `phone`/`phoneTruncated`, `address`/`addressTruncated`, `city`/`cityTruncated`, `countryCode`/`countryCodeTruncated`, `categoryName`/`categoryNameTruncated` | string/null, boolean | Pass-through fields bounded per **Input contract**'s length limits; a wrong-typed value is nulled instead and NOT reported as truncated. |
| `confidence` | number | Fixed heuristic: `0.65` for a group match; `1`/`0.85`/`0.6`/`0.5`/`0.4` for standalone, keyed by why grouping wasn't possible — never a calibrated probability. |
| `evidence` | array | Up to 50 entries, each `{ field, value, rule, sourceRowIndex }`. |
| `sourceRowCount` / `sourceRowIndexes` | number / array | True count and (capped at 50) list of contributing rows, including failed duplicates. |
| `duplicateSourceRowCount` | number | How many source rows shared this exact `placeId`. |
| `businessPartial` / `partial` | boolean | `businessPartial` is this module's own signal; `partial` also folds in run-level source incompleteness — check `partial` if you only check one. |
| `sourceCoverage` | object | Run-level: `{ complete, reason, droppedOversized }`. |
| `sourceDatasetId`, `sourceRunId`, `suppliedSourceRunId`, `sourceRunIdVerified` | string/null, boolean/null | Where the batch came from; `sourceRunId` set only when independently verified. |
| `processedAt` | string (ISO 8601) | When processed — not when scraped (see `scrapedAt` in your own rows). |
| `observedAt` | string (ISO 8601), null | The latest strictly calendar-valid `scrapedAt` across **every** accepted occurrence contributing to this location (not only the canonical row), computed before any display/evidence cap. The exact captured text, whitespace-trimmed only — never re-serialized, never re-formatted, never a processing-time/"now" fallback. `null` when no contributing occurrence carried a valid `scrapedAt`. |
| `sourceObservationWindow` | object | `{ earliest, latest, validCount, totalRowCount }` — `earliest`/`latest` are the raw `scrapedAt` strings as originally reported by the earliest/latest-valid occurrence, whitespace-trimmed only (never re-serialized/re-formatted); `validCount` is how many contributing occurrences had a strictly valid `scrapedAt`; `totalRowCount` is how many accepted occurrences contributed to this location at all — `validCount` is always ≤ `totalRowCount`, since an occurrence with an invalid, missing, or date-only `scrapedAt` still counts toward `totalRowCount` but never toward `validCount`. |
| `reason` / `error` | string | Present on free diagnostic rows only. |

**Complete runtime OUTPUT/SUMMARY field table** (the Key-value store record under both the `OUTPUT` and `SUMMARY` keys — identical payload, two conventional key names; no `metadata` field exists in this runtime's real output):

| Field | Meaning |
|---|---|
| `schemaVersion` | This product's output schema version string (`"1.0"`). |
| `product` | `"google-maps-chain-location-grouper"`. |
| `outcome` | `delivered`, `budget_stopped`, `max_results_reached`, `no_results`, `diagnostic`, or `failed`. |
| `errorCode` | Short internal code on a non-nominal outcome, or `null`. |
| `fatalFailure` | `true` only when `outcome === "failed"`. |
| `deliveryUncertain` | A Dataset-write or charge acknowledgment is uncertain, including FREE\_WRITE\_FAILED; raw rows can exceed confirmed counters. |
| `replayBlocked` | `true` on either of two distinct refusal causes: the run's own `result-found` event counter didn't match its expected record (`EXISTING_RESULT`), or this run's claim key was already set from a prior attempt and automatic resurrection is unsupported (`EXISTING_CLAIM`). |
| `requested` | Source rows actually read this run. |
| `usefulResults` | Results found after oversized-row filtering, before budget/`maxResults` cap. |
| `delivered` | Paid, confirmed assessments actually written. |
| `paid` | Confirmed `result-found` charge events, cross-checked against `delivered`. |
| `free` | Free diagnostic rows actually written — see **Evidence and boundaries** for how this can exceed the 200-row cap. |
| `failed` | Useful results dropped for exceeding the 64 KiB per-row limit (rare). |
| `sourcePartial` / `sourcePartialReason` | Whether the source READ was incomplete, and why. |
| `sourceDroppedOversized` | Source rows dropped for exceeding 64 KiB before reaching assessment logic. |
| `budgetStopped` | `true` if the charge limit was reached before all useful assessments were delivered. |
| `resultsCapped` | `true` if `maxResults` was reached before all useful assessments were delivered. |
| `businessPartialResults` | Partial transform results counted before size, budget and result-cap filtering; not a delivered-result count. |
| `oversizedDiagnosticsDropped` | Diagnostic rows dropped for exceeding 64 KiB (rare). |
| `diagnosticsTruncated` | `true` if more diagnostics existed than the runtime's write cap could hold — see **Evidence and boundaries**. |
| `partialOverall` | `true` if any of the above fired — check this one field if you only check one. |
| `budgetMode` | `unlimited`, `finite`, or `local_no_charging`. |
| `maxTotalChargeUsd` | The effective run charge cap enforced, or `null` when unlimited/local. |
| `replaySafety` | Fixed disclosure string: re-running is new processing, no cross-run dedup. |
| `businessSummary` | `{ totalSourceRows, resultsCount, diagnosticsCount, statusBreakdown, chainGroupsFound, observedAt }` — `diagnosticsCount` counts only this product's own business diagnostics, never run-level notices layered on top; `chainGroupsFound` is the true count of distinct `hostbrand:` group keys formed this run. |
| `processedAt` | ISO 8601 timestamp of this OUTPUT/SUMMARY write. |

### Evidence and boundaries

**Hard failure always wins, even against a contradictory `found:true, partial:true` claim.** A row with `found: false`, `failed: true`, or a `status` matching a known failure alias (`failed`/`error`/`timed-out`/`timedout`/`timeout`/`aborted`, trim+lowercase-normalized) is always a free `source_reported_failure` diagnostic — a source cannot simultaneously claim "this row failed" and "this row is a usable partial result." A non-empty `error`/`errors` field with no hard-failure signal present is retained as a harmless warning ONLY when the same row explicitly carries both `found: true` and `partial: true`; in every other case it still produces the free `source_reported_failure` diagnostic.

**A same-brand + same-hostname match is an observed relationship only, never a corporate-ownership claim.** `relationship: observed_brand_host_match` reflects that this batch's rows share a non-excluded hostname under a compatible normalized brand — nothing more. The shared host might be a generic hosting or site-builder domain not yet added to this Actor's finite exclusion list; `confidence: 0.65` is a fixed conservative heuristic, not a calibrated probability, and the action is `add_to_verification_queue`, never an automatic "confirmed chain" label.

**Shared platforms and phone numbers alone never link locations.** A hostname matching the built-in list (Facebook, Instagram, Wix, Squarespace, Yelp, and 25+ others, root-or-subdomain match) or your own `additionalExcludedHostnames` produces `status: excluded_platform` — an explicit, useful conclusion, distinct from an unresolved relationship. A shared phone number, address, or category is never itself grouping evidence — only hostname + compatible brand.

**A brand made up entirely of generic words is never clustering evidence.** A normalized brand that reduces to only `company`/`store`/`shop`/`business`/`group`/`market`/`services`/`shops`/`stores`-class tokens (articles ignored) cannot support a grouping or even a standalone "no peer" conclusion — it produces a free `unresolved_relationship` diagnostic, since a bare "Store" tells you nothing distinguishing about the business.

**An oversized normalized brand is rejected outright, never truncated.** A brand name over 500 characters becomes an unresolved relationship rather than a truncated matching key — truncating a matching key risks silently merging two genuinely different brands that share a long common prefix. The same applies to an oversized `placeId`/`cid` (`oversized_identity`) and an oversized extracted hostname (`invalid_url`, never truncated to fit).

**Prefix compatibility is word-boundary only, never a character-level substring match.** "metro cafe" is compatible with "metro cafe express" (every whole word of the shorter name matches, in order) — but "star" is never compatible with "starfish". Clustering also never transitively bridges through an intermediate-only-linked node: brand A compatible with generic B, and B compatible with unrelated C, never merges A and C unless they are themselves directly compatible.

**Duplicate identities are resolved deterministically, and a failed duplicate is disclosed, never scored.** When multiple rows share the same `placeId`/`cid`, the row with the most recent valid `scrapedAt` is canonical (else the last occurrence). A duplicate that itself reported a hard failure is excluded from evidence, but disclosed via `sourceRowCount` and `partial: true` on the surviving result. Canonical selection is a separate mechanism from `observedAt`/`sourceObservationWindow`: canonical selection decides which one occurrence's display fields (title, address, etc.) are used; the observation window spans `scrapedAt` across **every** accepted occurrence sharing the identity, whether or not that occurrence is the canonical one.

**Bounded, disclosed caps, applied only to display, never to the underlying determination:** `evidence` capped at 50 entries; `sourceRowIndexes` capped at 50; `groupMembers` capped at 50 (`groupSize` is the true total); pass-through fields bounded per **Input contract**; `groupMembers` title/address bounded far tighter (80/120 chars) since a member is a cross-reference, not a full record duplicate.

**Business diagnostics are capped at 200 written rows — but total free rows for a run can legitimately exceed that, and `businessSummary.diagnosticsCount` counts business-only.** The initial diagnostics array the shared runtime writes from consists of this product's own `report.diagnostics` PLUS the runtime's own `partial_source`/`source_rows_dropped` notices, combined then sliced to 200 written rows; `diagnosticsTruncated` covers that COMBINED list, while `businessSummary.diagnosticsCount` counts only this product's own business diagnostics, never the runtime notices layered on top. Separately, later notices — `result_dropped_oversized`, `budget_stopped`, or `max_results_reached` — are written on top of that 200-row cap and are not counted against it, raising the total free-row count above 200. Verified against a real 1,000-row batch (999 synthetic `source_reported_failure` rows plus a real 3-place batch tail, `maxResults: 1`): exactly 200 business diagnostics were written, one further `max_results_reached` notice was added, for **201 total free rows** (`freeRows: 201`) — never a blanket "200 free cap." `businessSummary.diagnosticsCount` in that same run reported `997` (the true diagnostic count before the 200-row write slice), not the count of rows actually written.

**Budget behavior — read this before relying on a specific `maxTotalChargeUsd` value via the API.** A positive `maxTotalChargeUsd` below the price of one assessment correctly delivers zero paid assessments and one free `budget_stopped` diagnostic (output example 3, $0.005 against a $0.0105 floor tier). Separately, the platform's own API-level cap-normalization behavior applies to a raw `maxTotalChargeUsd: 0` value. **A positive value below every tier's price only reliably guarantees zero paid results while the effective cap is preserved at that value — inspect the run's actual, effective `options.maxTotalChargeUsd` after it starts** rather than assuming the requested value silently held.

**Delivery is not transactional, and a `FAILED` run does not mean "nothing happened."** Each assessment is written to the Dataset and then charged as two separate steps; a run failing between them can leave an effectively free, unconfirmed write with no automatic retry — no cross-run exactly-once guarantee. If pricing/charge-limit/event-counter state can't be verified mid-run, the run finalizes `FAILED` with a safe explanation. **Inspect the original run's Dataset and `OUTPUT`/`SUMMARY` record before a manual retry** — a blind retry after an ambiguous outcome can duplicate paid assessments.

**Field values are read from the actual output, never assumed from the schema description alone.** Every field above was independently confirmed against this actor's own real evidence receipts — `businessSummary.diagnosticsCount` genuinely differs from `free` in the mixed-boundary receipt (`997` vs `201`), which only reading the real `summary.json` makes clear.

### Decision routing

- **`status: shared_brand_hostname_group`, `action: add_to_verification_queue`** — an observed hostname+brand correlation with at least one other place in this batch. Treat as a research lead to verify manually, not a confirmed relationship.
- **`status: standalone`, `action: no_action_standalone`** — a usable hostname and non-generic brand resolved, but no peer shares them in this batch — a real conclusion about this batch, not a claim of no chain relationships anywhere.
- **`status: excluded_platform`, `action: no_action_excluded_platform`** — the only website signal is a known shared platform; explicitly not usable as an ownership signal.
- **`partial: true`** — disclosed incompleteness, a duplicate-row conflict, a truncated/invalid field, or confidence below 1 — worth a second glance.
- **Free diagnostic rows (`found: false`)** — never an assessment. `missing_identity`/`oversized_identity` mean no stable identity; `unresolved_relationship` means neither a usable hostname nor a usable, non-generic brand was available; `invalid_domain_options` means your playbook input was rejected; `budget_stopped`/`max_results_reached` mean a run-level cap was hit.

### Commercial playbooks

**Multi-city account consolidation.** Run a fresh multi-city Maps export through to surface which listings likely belong to the same account before your reps start outreach.

**Franchise-development territory mapping.** Group a competitor or partner brand's Maps footprint by hostname+brand to estimate branch density per territory, using `groupSize` as a rough count.

**Data hygiene before CRM import.** Flag `shared_brand_hostname_group` results for manual account-merge review, and `duplicateSourceRowCount > 1` rows for de-duplication.

**Custom exclusion tuning.** Add industry-specific shared platforms via `additionalExcludedHostnames` so they never falsely inflate a `groupSize`.

### Integration recipes

![Google Maps Chain Location Grouper — workflow diagram](https://raw.githubusercontent.com/TimmyZinin/apify-actor-assets/d94b27ab62fe5f65d0258277507ea8d341761c8d/value-add-20260923/google-maps-chain-location-grouper/readme-workflow.webp)

This Actor's ID is **`6OruMc0vPsndzCW0s`**. A natural upstream source is [Google Maps Scraper](https://apify.com/compass/crawler-google-places) (Actor ID `nwua9Gu5YrADL7ZDj`); [Google Maps Extractor](https://apify.com/compass/google-maps-extractor) works identically. The recipes below describe how to configure each mechanism — not a claim any specific trigger is currently installed. A saved public example is available in the Tasks tab.

**1. Console-native chaining (no code).** On your Maps-scraper's run page, open **Integrations**, add an "Actor/task run" action targeting this Actor, and let the Integrations UI populate the payload with `{{resource.defaultDatasetId}}`. Map that value explicitly into this Actor's `datasetId` field — not the generic `payload` pass-through alone — to *request* a `READ` grant. The mapping requests the grant; it does not itself guarantee the platform honors it.

**2. Direct webhook (`POST /v2/webhooks`), full recipe.** Configure a webhook on your upstream Maps-scraper for the `ACTOR.RUN.SUCCEEDED` event, scoped to that specific Actor via its `condition.actorId`, targeting this Actor's run-creation endpoint:

```json
{
  "isApifyIntegration": true,
  "shouldInterpolateStrings": true,
  "eventTypes": ["ACTOR.RUN.SUCCEEDED"],
  "condition": { "actorId": "nwua9Gu5YrADL7ZDj" },
  "requestUrl": "https://api.apify.com/v2/acts/6OruMc0vPsndzCW0s/runs?maxTotalChargeUsd=0.50",
  "doNotRetry": true,
  "headersTemplate": "{\"Authorization\": \"Bearer {{APIFY_TOKEN}}\"}",
  "payloadTemplate": "{\"datasetId\": \"{{resource.defaultDatasetId}}\", \"payload\": {\"resource\": {\"defaultDatasetId\": \"{{resource.defaultDatasetId}}\", \"id\": \"{{resource.id}}\"}}, \"brandCompatibilityMode\": \"exact\"}"
}
```

Submit this to `POST https://api.apify.com/v2/webhooks` with your own `Authorization: Bearer <APIFY_TOKEN>` header. The `{{APIFY_TOKEN}}` placeholder is **not** an implicit platform template variable resolved automatically at trigger time — it must be provisioned separately: filled in from your own secrets store at the moment you register the webhook. `doNotRetry: true` avoids a duplicate-charge risk from an automatic retry. `datasetId` is set explicitly — that field is what actually requests access; `payload.resource` alone is informational provenance only. A positive `maxTotalChargeUsd` on the request URL caps spend; avoid literal `0` (see **Evidence and boundaries**).

**3. Generic automation tools (n8n, Zapier, Make, or your own tooling).** Use a generic Apify/HTTP node to call `run-sync-get-dataset-items` on this Actor's ID with a JSON body containing `datasetId` and your chosen `brandCompatibilityMode`/`additionalExcludedHostnames`/`maxResults` — a plain authenticated HTTPS POST, no native integration required.

**4. Export / CRM-adjacent mapping.** Every run's Dataset supports CSV/Excel/JSON export via the Console or `GET /v2/datasets/{datasetId}/items?format=csv`. `entityId`, `title`, `status`, `groupKey`, `groupSize`, and `hostname` alone drive a working worksheet.

### Operating guide

1. **Confirm your source batch shape first.** Spot-check rows against **Input contract** — each needs `placeId` or `cid`, and a `website`/`title` pair for any grouping signal at all.
2. **Choose `brandCompatibilityMode` deliberately.** `prefix` (default) catches more real variants but can occasionally overmatch; `exact` is stricter.
3. **Add your own excluded hostnames if your industry has a common shared platform** not in the built-in list — otherwise it can inflate `groupSize` falsely.
4. **Start with a small `maxResults` on a new source** to confirm output matches expectations before a full batch.
5. **Set `maxTotalChargeUsd` via the platform run option, and check its effective value after the run starts.** For a guaranteed-zero-paid API call, use a small positive value below $0.0105, not literal `0`.
6. **Read the `OUTPUT`/`SUMMARY` record, not just the Dataset row count** — remember `businessSummary.diagnosticsCount` counts only this product's own diagnostics, not run-level notices layered on top.
7. **If a run finalizes `FAILED`, inspect its Dataset and `OUTPUT` record before retrying** — a blind retry can duplicate already-successful paid assessments.
8. **Re-running is always new billing** — no cross-run dedup key.

### FAQ

**Does this Actor scrape Google Maps itself?** No. It only reads an existing places batch you or an upstream Actor already collected — no open-web fetch, no upstream Actor started.

**Can I use a source other than Google Maps Scraper?** Yes — any source producing rows with `placeId`/`cid`, `website`, and `title` works, including Google Maps Extractor.

**Does a `shared_brand_hostname_group` match mean these locations are confirmed corporately owned by the same company?** No. It means this batch's rows share a non-excluded hostname under a compatible normalized brand — an observed, batch-local correlation flagged for your own verification, never a legal-ownership or franchise-registry confirmation.

**Does a shared phone number group two locations?** No. Only hostname + compatible brand link locations; phone, address, and category are pass-through fields only.

**Why is a brand sometimes rejected even though the title looks fine?** If the normalized brand reduces to only generic words ("Store", "The Shop"), it carries no distinguishing information and can't support grouping — it's a free `unresolved_relationship` diagnostic instead.

**Does re-running the same batch avoid double-billing?** No. Every run is independent with no cross-run deduplication.

**Is the "partial-columns" style example a demonstration of missing upstream data?** No — see output example 2. Same real values as the healthy example; only `partial`/`found` are deliberately set as a controlled test.

**Why did one run report 201 free rows against a "200 diagnostics" cap, and diagnosticsCount showed 997?** A separate run-level notice (`max_results_reached`) is written on top of the 200-row cap, not counted against it. `businessSummary.diagnosticsCount` reported the true 997 count before that write slice — see **Evidence and boundaries**.

### Sources and rights

This Actor reads only Datasets and runs your own Apify account token already has permission to read; it never uses another user's credentials and never starts an upstream Actor on your behalf. The public prefilled example is a curated, non-sensitive real snapshot of three London museum listings from a Google Maps scrape (`sourceActorId nwua9Gu5YrADL7ZDj`, `sourceRunId 5VeVFvxu0MQhPBrWY`, `sourceDatasetId WJkSy7pXIRxQlwPvn`), observed 2026-09-22, reused with its original values unchanged — a saved example, not a new fresh scrape. All output values here are copied verbatim from real runs of this Actor's accepted build; none are invented. These verification runs were the product owner's own cost, not customer sales or revenue evidence.

#### Related tools

Related tools for adjacent workflows in B2B lead generation and data enrichment.

| Actor | What it does |
|---|---|
| [Company Lookup — Website & GLEIF Evidence](https://apify.com/zinin/company-lookup) | Pair it in the B2B lead generation and data enrichment workflow: Turn domains, company names, or exact LEIs into evidence-linked website and GLEIF observations with... |
| [Company Registry Enricher](https://apify.com/zinin/company-registry-enricher) | Pair it in the B2B lead generation and data enrichment workflow: Resolve company names, LEIs, or UK company numbers against official GLEIF and optional Companies House data |
| [Lead List Cleaner - Dedupe, DNS & Email Hygiene](https://apify.com/zinin/lead-list-cleaner) | Pair it in the B2B lead generation and data enrichment workflow: Normalize mixed lead rows, deduplicate domains and emails, check DNS/MX evidence, score retained leads,... |

| [Google Maps Scraper](https://apify.com/compass/crawler-google-places) | Collect upstream place rows. Check field compatibility before passing a saved Dataset to this Actor. |
| [Google Maps Reviews Scraper](https://apify.com/compass/google-maps-reviews-scraper) | Collect a separate review dataset for location research; review rows require their own workflow and are not chain-grouping inputs. |

# Actor input Schema

## `items` (type: `array`):

Small real public example: https://api.apify.com/v2/datasets/D7IDV0iPRUoMflfx3/items?clean=true . This saved snapshot is not a new scrape.

## `datasetId` (type: `string`):

Read an existing source dataset with your run permissions. Overrides inline items. The resource picker can request scoped READ permission. Integration mapping must use an authorized resource; a plain Dataset ID or webhook payload alone does not grant access. Public example: D7IDV0iPRUoMflfx3.

## `runId` (type: `string`):

Optional Actor run ID to read instead of Source Dataset. Its default Dataset is resolved with your run's own permissions — no elevated access is used, and no upstream Actor is started.

## `maxInputRows` (type: `integer`):

Maximum source records examined before grouping. At most 1000 rows, 4 MiB total and 64 KiB per row. Capped or incomplete source coverage is reported.

## `maxResults` (type: `integer`):

Maximum useful location assessment results delivered, independently of the platform Max total charge setting. Free diagnostics are reported separately.

## `payload` (type: `object`):

Optional ACTOR.RUN.SUCCEEDED event payload with resource.defaultDatasetId and resource.id. Dataset resourcePicker is recommended for private datasets; a payload alone does not grant access.

## `brandCompatibilityMode` (type: `string`):

How strictly normalized brand names must match before two locations sharing the same non-excluded hostname are linked into a chain group. 'prefix' allows e.g. "Metro Cafe" and "Metro Cafe Express" to link; 'exact' requires the normalized brand strings to be identical. Hostname match is always required either way — brand name alone never links locations.

## `additionalExcludedHostnames` (type: `array`):

Extra hostnames (booking platforms, marketplace subdomains, etc.) to treat as shared/non-ownership signals on top of the built-in list (social networks, common website builders, booking platforms).

## Actor input object example

```json
{
  "items": [
    {
      "title": "Natural History Museum",
      "subTitle": null,
      "description": "Landmark museum of animals and natural phenomena, with hands-on exhibits and animatronic dinosaurs.",
      "ownerDescription": null,
      "price": null,
      "categoryName": "Natural history museum",
      "address": "Cromwell Rd, South Kensington, London SW7 5BD, United Kingdom",
      "neighborhood": "South Kensington",
      "street": "Cromwell Rd",
      "city": "London",
      "postalCode": "SW7 5BD",
      "state": null,
      "countryCode": "GB",
      "website": "https://www.nhm.ac.uk/",
      "phone": "+44 20 7942 5000",
      "phoneUnformatted": "+442079425000",
      "location": {
        "lat": 51.496715,
        "lng": -0.1763672
      },
      "totalScore": 4.6,
      "reviewsCount": 39058,
      "permanentlyClosed": false,
      "temporarilyClosed": false,
      "placeId": "ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
      "cid": "8914844151054820548",
      "url": "https://www.google.com/maps/search/?api=1&query=Natural%20History%20Museum&query_place_id=ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
      "scrapedAt": "2026-09-22T23:07:38.397Z",
      "additionalInfo": {
        "Accessibility": [
          {
            "Wheelchair accessible entrance": true
          },
          {
            "Wheelchair accessible parking lot": true
          },
          {
            "Wheelchair accessible restroom": true
          },
          {
            "Wheelchair accessible seating": true
          }
        ]
      },
      "_exampleProvenance": {
        "sourceActorId": "nwua9Gu5YrADL7ZDj",
        "sourceRunId": "5VeVFvxu0MQhPBrWY",
        "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
        "curatedPublicSnapshot": true,
        "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities."
      }
    },
    {
      "title": "Museum of Illusions",
      "subTitle": null,
      "description": null,
      "ownerDescription": "Come challenge your senses at Museum of Illusions London, an interactive museum and immersive experience located on Tottenham Court Road in the heart of London. Explore a must see attraction that blends museum with illusion rooms, installations and holograms that make it one of the top attractions for anyone looking for fun things to do in London. Perfect for kids activities, family fun, birthday parties, field trips, date night, group activities, teambuilding and corporate events, it is a family friendly museum where educational fun meets unforgettable entertainment. Defy gravity, grow and shrink your body, walk into a life-size kaleidoscope and play tricks on your senses at every turn.",
      "price": null,
      "categoryName": "Museum",
      "address": "6, 17 Tottenham Ct Rd, London W1T 1BJ, United Kingdom",
      "neighborhood": null,
      "street": "6, 17 Tottenham Ct Rd",
      "city": "London",
      "postalCode": "W1T 1BJ",
      "state": null,
      "countryCode": "GB",
      "website": "https://moilondon.uk/?utm_source=google&utm_medium=local&utm_campaign=uberall",
      "phone": "",
      "phoneUnformatted": "",
      "location": {
        "lat": 51.5174418,
        "lng": -0.1316447
      },
      "totalScore": 4.8,
      "reviewsCount": 1015,
      "permanentlyClosed": false,
      "temporarilyClosed": false,
      "placeId": "ChIJsS5McdcbdkgR9rc9sUf1ZS4",
      "cid": "3343347986657556470",
      "url": "https://www.google.com/maps/search/?api=1&query=Museum%20of%20Illusions&query_place_id=ChIJsS5McdcbdkgR9rc9sUf1ZS4",
      "scrapedAt": "2026-09-22T23:07:41.959Z",
      "additionalInfo": {
        "Service options": [
          {
            "Onsite services": true
          }
        ],
        "Accessibility": [
          {
            "Assisted listening devices": true
          },
          {
            "Wheelchair accessible entrance": true
          },
          {
            "Wheelchair accessible restroom": true
          }
        ],
        "Amenities": [
          {
            "Offers tours": true
          },
          {
            "Restroom": true
          },
          {
            "Wi-Fi": true
          },
          {
            "Free Wi-Fi": true
          }
        ],
        "Crowd": [
          {
            "Family-friendly": true
          }
        ],
        "Planning": [
          {
            "Getting tickets in advance recommended": true
          }
        ],
        "Payments": [
          {
            "Admission fee": true
          }
        ],
        "Children": [
          {
            "Discounts for kids": true
          },
          {
            "Family discount": true
          },
          {
            "Good for kids": true
          },
          {
            "Kid-friendly activities": true
          }
        ]
      },
      "_exampleProvenance": {
        "sourceActorId": "nwua9Gu5YrADL7ZDj",
        "sourceRunId": "5VeVFvxu0MQhPBrWY",
        "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
        "curatedPublicSnapshot": true,
        "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities."
      }
    },
    {
      "title": "Science Museum",
      "subTitle": null,
      "description": "A vast treasury of science & invention, famous for its state-of-the-art interactive exhibits.",
      "ownerDescription": "Free to visit, the Science Museum is a must-see as part of any trip to London, with award-winning exhibitions, iconic objects and stories of incredible scientific achievement",
      "price": null,
      "categoryName": "Museum",
      "address": "Exhibition Rd, South Kensington, London SW7 2DD, United Kingdom",
      "neighborhood": "South Kensington",
      "street": "Exhibition Rd",
      "city": "London",
      "postalCode": "SW7 2DD",
      "state": null,
      "countryCode": "GB",
      "website": "https://www.sciencemuseum.org.uk/",
      "phone": "+44 330 058 0058",
      "phoneUnformatted": "+443300580058",
      "location": {
        "lat": 51.4972216,
        "lng": -0.1767283
      },
      "totalScore": 4.6,
      "reviewsCount": 77054,
      "permanentlyClosed": false,
      "temporarilyClosed": false,
      "placeId": "ChIJP9oAE0MFdkgR3iKGFKZO1SE",
      "cid": "2437941248506864350",
      "url": "https://www.google.com/maps/search/?api=1&query=Science%20Museum&query_place_id=ChIJP9oAE0MFdkgR3iKGFKZO1SE",
      "scrapedAt": "2026-09-22T23:07:42.214Z",
      "additionalInfo": {
        "Service options": [
          {
            "Onsite services": true
          }
        ],
        "Highlights": [
          {
            "Live performances": true
          }
        ],
        "Accessibility": [
          {
            "Wheelchair accessible entrance": true
          },
          {
            "Wheelchair accessible parking lot": true
          },
          {
            "Wheelchair accessible restroom": true
          },
          {
            "Wheelchair accessible seating": true
          }
        ],
        "Offerings": [
          {
            "Gift shop": true
          },
          {
            "Theater": true
          }
        ],
        "Amenities": [
          {
            "Restaurant": true
          },
          {
            "Restroom": true
          },
          {
            "Wi-Fi": true
          },
          {
            "Free Wi-Fi": true
          }
        ],
        "Crowd": [
          {
            "Family-friendly": true
          }
        ],
        "Planning": [
          {
            "Getting tickets in advance recommended": true
          }
        ],
        "Children": [
          {
            "Good for kids": true
          },
          {
            "Has changing table(s)": true
          }
        ],
        "Parking": [
          {
            "Free parking lot": true
          },
          {
            "Paid parking lot": true
          },
          {
            "Paid street parking": true
          }
        ]
      },
      "_exampleProvenance": {
        "sourceActorId": "nwua9Gu5YrADL7ZDj",
        "sourceRunId": "5VeVFvxu0MQhPBrWY",
        "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
        "curatedPublicSnapshot": true,
        "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities."
      }
    }
  ],
  "maxInputRows": 100,
  "maxResults": 10,
  "brandCompatibilityMode": "prefix",
  "additionalExcludedHostnames": []
}
```

# Actor output Schema

## `results` (type: `string`):

API URL for the default dataset items produced by this run.

## `summary` (type: `string`):

API URL for the OUTPUT record in the default 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 = {
    "items": [
        {
            "title": "Natural History Museum",
            "subTitle": null,
            "description": "Landmark museum of animals and natural phenomena, with hands-on exhibits and animatronic dinosaurs.",
            "ownerDescription": null,
            "price": null,
            "categoryName": "Natural history museum",
            "address": "Cromwell Rd, South Kensington, London SW7 5BD, United Kingdom",
            "neighborhood": "South Kensington",
            "street": "Cromwell Rd",
            "city": "London",
            "postalCode": "SW7 5BD",
            "state": null,
            "countryCode": "GB",
            "website": "https://www.nhm.ac.uk/",
            "phone": "+44 20 7942 5000",
            "phoneUnformatted": "+442079425000",
            "location": {
                "lat": 51.496715,
                "lng": -0.1763672
            },
            "totalScore": 4.6,
            "reviewsCount": 39058,
            "permanentlyClosed": false,
            "temporarilyClosed": false,
            "placeId": "ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
            "cid": "8914844151054820548",
            "url": "https://www.google.com/maps/search/?api=1&query=Natural%20History%20Museum&query_place_id=ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
            "scrapedAt": "2026-09-22T23:07:38.397Z",
            "additionalInfo": {
                "Accessibility": [
                    {
                        "Wheelchair accessible entrance": true
                    },
                    {
                        "Wheelchair accessible parking lot": true
                    },
                    {
                        "Wheelchair accessible restroom": true
                    },
                    {
                        "Wheelchair accessible seating": true
                    }
                ]
            },
            "_exampleProvenance": {
                "sourceActorId": "nwua9Gu5YrADL7ZDj",
                "sourceRunId": "5VeVFvxu0MQhPBrWY",
                "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
                "curatedPublicSnapshot": true,
                "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities."
            }
        },
        {
            "title": "Museum of Illusions",
            "subTitle": null,
            "description": null,
            "ownerDescription": "Come challenge your senses at Museum of Illusions London, an interactive museum and immersive experience located on Tottenham Court Road in the heart of London. Explore a must see attraction that blends museum with illusion rooms, installations and holograms that make it one of the top attractions for anyone looking for fun things to do in London. Perfect for kids activities, family fun, birthday parties, field trips, date night, group activities, teambuilding and corporate events, it is a family friendly museum where educational fun meets unforgettable entertainment. Defy gravity, grow and shrink your body, walk into a life-size kaleidoscope and play tricks on your senses at every turn.",
            "price": null,
            "categoryName": "Museum",
            "address": "6, 17 Tottenham Ct Rd, London W1T 1BJ, United Kingdom",
            "neighborhood": null,
            "street": "6, 17 Tottenham Ct Rd",
            "city": "London",
            "postalCode": "W1T 1BJ",
            "state": null,
            "countryCode": "GB",
            "website": "https://moilondon.uk/?utm_source=google&utm_medium=local&utm_campaign=uberall",
            "phone": "",
            "phoneUnformatted": "",
            "location": {
                "lat": 51.5174418,
                "lng": -0.1316447
            },
            "totalScore": 4.8,
            "reviewsCount": 1015,
            "permanentlyClosed": false,
            "temporarilyClosed": false,
            "placeId": "ChIJsS5McdcbdkgR9rc9sUf1ZS4",
            "cid": "3343347986657556470",
            "url": "https://www.google.com/maps/search/?api=1&query=Museum%20of%20Illusions&query_place_id=ChIJsS5McdcbdkgR9rc9sUf1ZS4",
            "scrapedAt": "2026-09-22T23:07:41.959Z",
            "additionalInfo": {
                "Service options": [
                    {
                        "Onsite services": true
                    }
                ],
                "Accessibility": [
                    {
                        "Assisted listening devices": true
                    },
                    {
                        "Wheelchair accessible entrance": true
                    },
                    {
                        "Wheelchair accessible restroom": true
                    }
                ],
                "Amenities": [
                    {
                        "Offers tours": true
                    },
                    {
                        "Restroom": true
                    },
                    {
                        "Wi-Fi": true
                    },
                    {
                        "Free Wi-Fi": true
                    }
                ],
                "Crowd": [
                    {
                        "Family-friendly": true
                    }
                ],
                "Planning": [
                    {
                        "Getting tickets in advance recommended": true
                    }
                ],
                "Payments": [
                    {
                        "Admission fee": true
                    }
                ],
                "Children": [
                    {
                        "Discounts for kids": true
                    },
                    {
                        "Family discount": true
                    },
                    {
                        "Good for kids": true
                    },
                    {
                        "Kid-friendly activities": true
                    }
                ]
            },
            "_exampleProvenance": {
                "sourceActorId": "nwua9Gu5YrADL7ZDj",
                "sourceRunId": "5VeVFvxu0MQhPBrWY",
                "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
                "curatedPublicSnapshot": true,
                "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities."
            }
        },
        {
            "title": "Science Museum",
            "subTitle": null,
            "description": "A vast treasury of science & invention, famous for its state-of-the-art interactive exhibits.",
            "ownerDescription": "Free to visit, the Science Museum is a must-see as part of any trip to London, with award-winning exhibitions, iconic objects and stories of incredible scientific achievement",
            "price": null,
            "categoryName": "Museum",
            "address": "Exhibition Rd, South Kensington, London SW7 2DD, United Kingdom",
            "neighborhood": "South Kensington",
            "street": "Exhibition Rd",
            "city": "London",
            "postalCode": "SW7 2DD",
            "state": null,
            "countryCode": "GB",
            "website": "https://www.sciencemuseum.org.uk/",
            "phone": "+44 330 058 0058",
            "phoneUnformatted": "+443300580058",
            "location": {
                "lat": 51.4972216,
                "lng": -0.1767283
            },
            "totalScore": 4.6,
            "reviewsCount": 77054,
            "permanentlyClosed": false,
            "temporarilyClosed": false,
            "placeId": "ChIJP9oAE0MFdkgR3iKGFKZO1SE",
            "cid": "2437941248506864350",
            "url": "https://www.google.com/maps/search/?api=1&query=Science%20Museum&query_place_id=ChIJP9oAE0MFdkgR3iKGFKZO1SE",
            "scrapedAt": "2026-09-22T23:07:42.214Z",
            "additionalInfo": {
                "Service options": [
                    {
                        "Onsite services": true
                    }
                ],
                "Highlights": [
                    {
                        "Live performances": true
                    }
                ],
                "Accessibility": [
                    {
                        "Wheelchair accessible entrance": true
                    },
                    {
                        "Wheelchair accessible parking lot": true
                    },
                    {
                        "Wheelchair accessible restroom": true
                    },
                    {
                        "Wheelchair accessible seating": true
                    }
                ],
                "Offerings": [
                    {
                        "Gift shop": true
                    },
                    {
                        "Theater": true
                    }
                ],
                "Amenities": [
                    {
                        "Restaurant": true
                    },
                    {
                        "Restroom": true
                    },
                    {
                        "Wi-Fi": true
                    },
                    {
                        "Free Wi-Fi": true
                    }
                ],
                "Crowd": [
                    {
                        "Family-friendly": true
                    }
                ],
                "Planning": [
                    {
                        "Getting tickets in advance recommended": true
                    }
                ],
                "Children": [
                    {
                        "Good for kids": true
                    },
                    {
                        "Has changing table(s)": true
                    }
                ],
                "Parking": [
                    {
                        "Free parking lot": true
                    },
                    {
                        "Paid parking lot": true
                    },
                    {
                        "Paid street parking": true
                    }
                ]
            },
            "_exampleProvenance": {
                "sourceActorId": "nwua9Gu5YrADL7ZDj",
                "sourceRunId": "5VeVFvxu0MQhPBrWY",
                "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
                "curatedPublicSnapshot": true,
                "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities."
            }
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("zinin/google-maps-chain-location-grouper").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 = { "items": [
        {
            "title": "Natural History Museum",
            "subTitle": None,
            "description": "Landmark museum of animals and natural phenomena, with hands-on exhibits and animatronic dinosaurs.",
            "ownerDescription": None,
            "price": None,
            "categoryName": "Natural history museum",
            "address": "Cromwell Rd, South Kensington, London SW7 5BD, United Kingdom",
            "neighborhood": "South Kensington",
            "street": "Cromwell Rd",
            "city": "London",
            "postalCode": "SW7 5BD",
            "state": None,
            "countryCode": "GB",
            "website": "https://www.nhm.ac.uk/",
            "phone": "+44 20 7942 5000",
            "phoneUnformatted": "+442079425000",
            "location": {
                "lat": 51.496715,
                "lng": -0.1763672,
            },
            "totalScore": 4.6,
            "reviewsCount": 39058,
            "permanentlyClosed": False,
            "temporarilyClosed": False,
            "placeId": "ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
            "cid": "8914844151054820548",
            "url": "https://www.google.com/maps/search/?api=1&query=Natural%20History%20Museum&query_place_id=ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
            "scrapedAt": "2026-09-22T23:07:38.397Z",
            "additionalInfo": { "Accessibility": [
                    { "Wheelchair accessible entrance": True },
                    { "Wheelchair accessible parking lot": True },
                    { "Wheelchair accessible restroom": True },
                    { "Wheelchair accessible seating": True },
                ] },
            "_exampleProvenance": {
                "sourceActorId": "nwua9Gu5YrADL7ZDj",
                "sourceRunId": "5VeVFvxu0MQhPBrWY",
                "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
                "curatedPublicSnapshot": True,
                "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities.",
            },
        },
        {
            "title": "Museum of Illusions",
            "subTitle": None,
            "description": None,
            "ownerDescription": "Come challenge your senses at Museum of Illusions London, an interactive museum and immersive experience located on Tottenham Court Road in the heart of London. Explore a must see attraction that blends museum with illusion rooms, installations and holograms that make it one of the top attractions for anyone looking for fun things to do in London. Perfect for kids activities, family fun, birthday parties, field trips, date night, group activities, teambuilding and corporate events, it is a family friendly museum where educational fun meets unforgettable entertainment. Defy gravity, grow and shrink your body, walk into a life-size kaleidoscope and play tricks on your senses at every turn.",
            "price": None,
            "categoryName": "Museum",
            "address": "6, 17 Tottenham Ct Rd, London W1T 1BJ, United Kingdom",
            "neighborhood": None,
            "street": "6, 17 Tottenham Ct Rd",
            "city": "London",
            "postalCode": "W1T 1BJ",
            "state": None,
            "countryCode": "GB",
            "website": "https://moilondon.uk/?utm_source=google&utm_medium=local&utm_campaign=uberall",
            "phone": "",
            "phoneUnformatted": "",
            "location": {
                "lat": 51.5174418,
                "lng": -0.1316447,
            },
            "totalScore": 4.8,
            "reviewsCount": 1015,
            "permanentlyClosed": False,
            "temporarilyClosed": False,
            "placeId": "ChIJsS5McdcbdkgR9rc9sUf1ZS4",
            "cid": "3343347986657556470",
            "url": "https://www.google.com/maps/search/?api=1&query=Museum%20of%20Illusions&query_place_id=ChIJsS5McdcbdkgR9rc9sUf1ZS4",
            "scrapedAt": "2026-09-22T23:07:41.959Z",
            "additionalInfo": {
                "Service options": [{ "Onsite services": True }],
                "Accessibility": [
                    { "Assisted listening devices": True },
                    { "Wheelchair accessible entrance": True },
                    { "Wheelchair accessible restroom": True },
                ],
                "Amenities": [
                    { "Offers tours": True },
                    { "Restroom": True },
                    { "Wi-Fi": True },
                    { "Free Wi-Fi": True },
                ],
                "Crowd": [{ "Family-friendly": True }],
                "Planning": [{ "Getting tickets in advance recommended": True }],
                "Payments": [{ "Admission fee": True }],
                "Children": [
                    { "Discounts for kids": True },
                    { "Family discount": True },
                    { "Good for kids": True },
                    { "Kid-friendly activities": True },
                ],
            },
            "_exampleProvenance": {
                "sourceActorId": "nwua9Gu5YrADL7ZDj",
                "sourceRunId": "5VeVFvxu0MQhPBrWY",
                "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
                "curatedPublicSnapshot": True,
                "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities.",
            },
        },
        {
            "title": "Science Museum",
            "subTitle": None,
            "description": "A vast treasury of science & invention, famous for its state-of-the-art interactive exhibits.",
            "ownerDescription": "Free to visit, the Science Museum is a must-see as part of any trip to London, with award-winning exhibitions, iconic objects and stories of incredible scientific achievement",
            "price": None,
            "categoryName": "Museum",
            "address": "Exhibition Rd, South Kensington, London SW7 2DD, United Kingdom",
            "neighborhood": "South Kensington",
            "street": "Exhibition Rd",
            "city": "London",
            "postalCode": "SW7 2DD",
            "state": None,
            "countryCode": "GB",
            "website": "https://www.sciencemuseum.org.uk/",
            "phone": "+44 330 058 0058",
            "phoneUnformatted": "+443300580058",
            "location": {
                "lat": 51.4972216,
                "lng": -0.1767283,
            },
            "totalScore": 4.6,
            "reviewsCount": 77054,
            "permanentlyClosed": False,
            "temporarilyClosed": False,
            "placeId": "ChIJP9oAE0MFdkgR3iKGFKZO1SE",
            "cid": "2437941248506864350",
            "url": "https://www.google.com/maps/search/?api=1&query=Science%20Museum&query_place_id=ChIJP9oAE0MFdkgR3iKGFKZO1SE",
            "scrapedAt": "2026-09-22T23:07:42.214Z",
            "additionalInfo": {
                "Service options": [{ "Onsite services": True }],
                "Highlights": [{ "Live performances": True }],
                "Accessibility": [
                    { "Wheelchair accessible entrance": True },
                    { "Wheelchair accessible parking lot": True },
                    { "Wheelchair accessible restroom": True },
                    { "Wheelchair accessible seating": True },
                ],
                "Offerings": [
                    { "Gift shop": True },
                    { "Theater": True },
                ],
                "Amenities": [
                    { "Restaurant": True },
                    { "Restroom": True },
                    { "Wi-Fi": True },
                    { "Free Wi-Fi": True },
                ],
                "Crowd": [{ "Family-friendly": True }],
                "Planning": [{ "Getting tickets in advance recommended": True }],
                "Children": [
                    { "Good for kids": True },
                    { "Has changing table(s)": True },
                ],
                "Parking": [
                    { "Free parking lot": True },
                    { "Paid parking lot": True },
                    { "Paid street parking": True },
                ],
            },
            "_exampleProvenance": {
                "sourceActorId": "nwua9Gu5YrADL7ZDj",
                "sourceRunId": "5VeVFvxu0MQhPBrWY",
                "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
                "curatedPublicSnapshot": True,
                "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities.",
            },
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("zinin/google-maps-chain-location-grouper").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 '{
  "items": [
    {
      "title": "Natural History Museum",
      "subTitle": null,
      "description": "Landmark museum of animals and natural phenomena, with hands-on exhibits and animatronic dinosaurs.",
      "ownerDescription": null,
      "price": null,
      "categoryName": "Natural history museum",
      "address": "Cromwell Rd, South Kensington, London SW7 5BD, United Kingdom",
      "neighborhood": "South Kensington",
      "street": "Cromwell Rd",
      "city": "London",
      "postalCode": "SW7 5BD",
      "state": null,
      "countryCode": "GB",
      "website": "https://www.nhm.ac.uk/",
      "phone": "+44 20 7942 5000",
      "phoneUnformatted": "+442079425000",
      "location": {
        "lat": 51.496715,
        "lng": -0.1763672
      },
      "totalScore": 4.6,
      "reviewsCount": 39058,
      "permanentlyClosed": false,
      "temporarilyClosed": false,
      "placeId": "ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
      "cid": "8914844151054820548",
      "url": "https://www.google.com/maps/search/?api=1&query=Natural%20History%20Museum&query_place_id=ChIJPy8Y5kIFdkgRxGSXw4Xjt3s",
      "scrapedAt": "2026-09-22T23:07:38.397Z",
      "additionalInfo": {
        "Accessibility": [
          {
            "Wheelchair accessible entrance": true
          },
          {
            "Wheelchair accessible parking lot": true
          },
          {
            "Wheelchair accessible restroom": true
          },
          {
            "Wheelchair accessible seating": true
          }
        ]
      },
      "_exampleProvenance": {
        "sourceActorId": "nwua9Gu5YrADL7ZDj",
        "sourceRunId": "5VeVFvxu0MQhPBrWY",
        "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
        "curatedPublicSnapshot": true,
        "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities."
      }
    },
    {
      "title": "Museum of Illusions",
      "subTitle": null,
      "description": null,
      "ownerDescription": "Come challenge your senses at Museum of Illusions London, an interactive museum and immersive experience located on Tottenham Court Road in the heart of London. Explore a must see attraction that blends museum with illusion rooms, installations and holograms that make it one of the top attractions for anyone looking for fun things to do in London. Perfect for kids activities, family fun, birthday parties, field trips, date night, group activities, teambuilding and corporate events, it is a family friendly museum where educational fun meets unforgettable entertainment. Defy gravity, grow and shrink your body, walk into a life-size kaleidoscope and play tricks on your senses at every turn.",
      "price": null,
      "categoryName": "Museum",
      "address": "6, 17 Tottenham Ct Rd, London W1T 1BJ, United Kingdom",
      "neighborhood": null,
      "street": "6, 17 Tottenham Ct Rd",
      "city": "London",
      "postalCode": "W1T 1BJ",
      "state": null,
      "countryCode": "GB",
      "website": "https://moilondon.uk/?utm_source=google&utm_medium=local&utm_campaign=uberall",
      "phone": "",
      "phoneUnformatted": "",
      "location": {
        "lat": 51.5174418,
        "lng": -0.1316447
      },
      "totalScore": 4.8,
      "reviewsCount": 1015,
      "permanentlyClosed": false,
      "temporarilyClosed": false,
      "placeId": "ChIJsS5McdcbdkgR9rc9sUf1ZS4",
      "cid": "3343347986657556470",
      "url": "https://www.google.com/maps/search/?api=1&query=Museum%20of%20Illusions&query_place_id=ChIJsS5McdcbdkgR9rc9sUf1ZS4",
      "scrapedAt": "2026-09-22T23:07:41.959Z",
      "additionalInfo": {
        "Service options": [
          {
            "Onsite services": true
          }
        ],
        "Accessibility": [
          {
            "Assisted listening devices": true
          },
          {
            "Wheelchair accessible entrance": true
          },
          {
            "Wheelchair accessible restroom": true
          }
        ],
        "Amenities": [
          {
            "Offers tours": true
          },
          {
            "Restroom": true
          },
          {
            "Wi-Fi": true
          },
          {
            "Free Wi-Fi": true
          }
        ],
        "Crowd": [
          {
            "Family-friendly": true
          }
        ],
        "Planning": [
          {
            "Getting tickets in advance recommended": true
          }
        ],
        "Payments": [
          {
            "Admission fee": true
          }
        ],
        "Children": [
          {
            "Discounts for kids": true
          },
          {
            "Family discount": true
          },
          {
            "Good for kids": true
          },
          {
            "Kid-friendly activities": true
          }
        ]
      },
      "_exampleProvenance": {
        "sourceActorId": "nwua9Gu5YrADL7ZDj",
        "sourceRunId": "5VeVFvxu0MQhPBrWY",
        "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
        "curatedPublicSnapshot": true,
        "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities."
      }
    },
    {
      "title": "Science Museum",
      "subTitle": null,
      "description": "A vast treasury of science & invention, famous for its state-of-the-art interactive exhibits.",
      "ownerDescription": "Free to visit, the Science Museum is a must-see as part of any trip to London, with award-winning exhibitions, iconic objects and stories of incredible scientific achievement",
      "price": null,
      "categoryName": "Museum",
      "address": "Exhibition Rd, South Kensington, London SW7 2DD, United Kingdom",
      "neighborhood": "South Kensington",
      "street": "Exhibition Rd",
      "city": "London",
      "postalCode": "SW7 2DD",
      "state": null,
      "countryCode": "GB",
      "website": "https://www.sciencemuseum.org.uk/",
      "phone": "+44 330 058 0058",
      "phoneUnformatted": "+443300580058",
      "location": {
        "lat": 51.4972216,
        "lng": -0.1767283
      },
      "totalScore": 4.6,
      "reviewsCount": 77054,
      "permanentlyClosed": false,
      "temporarilyClosed": false,
      "placeId": "ChIJP9oAE0MFdkgR3iKGFKZO1SE",
      "cid": "2437941248506864350",
      "url": "https://www.google.com/maps/search/?api=1&query=Science%20Museum&query_place_id=ChIJP9oAE0MFdkgR3iKGFKZO1SE",
      "scrapedAt": "2026-09-22T23:07:42.214Z",
      "additionalInfo": {
        "Service options": [
          {
            "Onsite services": true
          }
        ],
        "Highlights": [
          {
            "Live performances": true
          }
        ],
        "Accessibility": [
          {
            "Wheelchair accessible entrance": true
          },
          {
            "Wheelchair accessible parking lot": true
          },
          {
            "Wheelchair accessible restroom": true
          },
          {
            "Wheelchair accessible seating": true
          }
        ],
        "Offerings": [
          {
            "Gift shop": true
          },
          {
            "Theater": true
          }
        ],
        "Amenities": [
          {
            "Restaurant": true
          },
          {
            "Restroom": true
          },
          {
            "Wi-Fi": true
          },
          {
            "Free Wi-Fi": true
          }
        ],
        "Crowd": [
          {
            "Family-friendly": true
          }
        ],
        "Planning": [
          {
            "Getting tickets in advance recommended": true
          }
        ],
        "Children": [
          {
            "Good for kids": true
          },
          {
            "Has changing table(s)": true
          }
        ],
        "Parking": [
          {
            "Free parking lot": true
          },
          {
            "Paid parking lot": true
          },
          {
            "Paid street parking": true
          }
        ]
      },
      "_exampleProvenance": {
        "sourceActorId": "nwua9Gu5YrADL7ZDj",
        "sourceRunId": "5VeVFvxu0MQhPBrWY",
        "sourceDatasetId": "WJkSy7pXIRxQlwPvn",
        "curatedPublicSnapshot": true,
        "redaction": "Allowlisted public business/product fields only; review excerpts and social captions truncated; no reviewer identities."
      }
    }
  ]
}' |
apify call zinin/google-maps-chain-location-grouper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,zinin/google-maps-chain-location-grouper"
        }
    }
}
```

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/6OruMc0vPsndzCW0s/builds/2jkDPQDGYEmdNXXbV/openapi.json
