# Google Maps Scraper Deduper | Merge Duplicates | Cross-Run (`broomwagon/google-maps-deduper`) Actor

Merge duplicate places from Google Maps scraper runs — by placeId, phone, or fuzzy name+address — including duplicates from previous runs. Only get what's new, cut enrichment costs, and keep a full audit trail. Stateless: you hold the memory, we store nothing.

- **URL**: https://apify.com/broomwagon/google-maps-deduper.md
- **Developed by:** [Brandon Mensing](https://apify.com/broomwagon) (community)
- **Categories:** Automation, Lead generation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 record processeds

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Google Maps Scraper Deduper

<img align="right" src="https://broomwagon.vercel.app/icons/brand-mark.png" width="64" alt="broomwagon.">

> Merge duplicate places from Google Maps Scraper results — by placeId, phone, or fuzzy name+address — including duplicates from previous runs.

Google Maps scrapers structurally produce duplicates: overlapping search terms, the
5,000-review chunking rule, and re-runs of the same area all put the same place in your
dataset more than once. Apify's own support guidance is to "post-process the scraped data
to remove duplicates based on phone numbers or other criteria." This actor is that
post-processing step.

Part of the **Broomwagon** family: deterministic tools that watch scraped data change.
Same input, same output, every run — with a full audit trail.

### Quick start: your first two runs

The whole product is one loop — run, keep the memory, run again, get only what's new.
Try it in two minutes with no dataset needed:

**Run 1.** Paste this into **Inline items** and start the run:

```json
[{"title":"Kopi Cafe","address":"5317 N Clark St, Chicago","phone":"(773) 989-5674"},
 {"title":"La Colombe","address":"5158 N Clark St, Chicago","phone":"(773) 293-6048"}]
```

You get both places back, plus — in the run's **Storage → Key-value store** — a record
called **`MEMORY`**. That's the actor's memory of what it has delivered to you, and
you hold it (we store nothing).

**Run 2.** Copy the whole `MEMORY` object into the **Previous memory** field, add a
third place to **Inline items**, and run again:

```json
[{"title":"Kopi Cafe","address":"5317 N Clark St, Chicago","phone":"(773) 989-5674"},
 {"title":"La Colombe","address":"5158 N Clark St, Chicago","phone":"(773) 293-6048"},
 {"title":"Lost Larson","address":"5318 N Clark St, Chicago","phone":"(773) 944-0587"}]
```

The output contains **only Lost Larson** — the two places you already received are
dropped as `dropped-prior-run`. In real use, replace Inline items with your Google
Maps scraper run's **Dataset**, and either keep passing the memory forward (n8n, Make,
API) or set **Memory key-value store** once and let Apify Schedules chain runs
automatically.

### Dedupe before you enrich

Enrichment events (email verification, contact lookup) typically cost 25x the scrape
price. On a documented community run with 5,000 leads and ~700 unique places, enriching
raw costs $500; enriching deduped costs $70. Dedupe pays for itself on the first run —
and prevents double-sends to the same prospect afterward.

### Cross-run dedupe (the part nothing else does)

In-scraper dedupe only works within a single run. This actor remembers across runs
WITHOUT storing your data: every run returns a compact aggregate (the **memory**, in
the `MEMORY` key-value record) containing a Bloom-filter ledger of every place key
you've been delivered. Pass it back next run and previously delivered places are
dropped — "only what's new since last time."

- **Inline mode:** pass last run's `MEMORY` record as `memory` (works in n8n,
  Make, API calls, and agent workflows).
- **Store mode:** pass `memoryStoreId` (a key-value store in your account) and the
  actor chains runs automatically — bare Apify Schedules just work.

Your data never lives in Broomwagon storage. The ledger is approximate by design: at
the default settings, up to 1% of genuinely new places may be flagged as already seen
(configurable via `ledgerCapacity`; the false-positive estimate ships in every report).

### How matching works

1. **Exact:** `placeId`/`cid`/`fid`, then normalized phone (`(718) 356-5168` ==
   `+17183565168`), then normalized name+address.
2. **Fuzzy:** places in the same city/postal block merge when Jaro-Winkler name
   similarity clears `fuzzyThreshold` (default 92) and address similarity clears
   threshold minus 5. "Verve Coffee" @ "2101 Pacific Ave" merges with "Verve Coffee
   Roasters" @ "2101 Pacific Avenue".
3. **Cross-run:** ledger hits from previous runs are dropped and counted separately.

Every decision lands in the `AUDIT` record: kept, merged-within-run (with match reason
and score), or dropped-prior-run.

### Input

- `datasetId`: a Google Maps scraper run's dataset (or `items` inline / `fileUrl`).
- `memory` / `memoryStoreId`: cross-run memory, as above.
- `fuzzyThreshold` (50-100, default 92), `ledgerCapacity` (default 100,000).

### Output

- Dataset: your places, deduplicated, original records untouched.
- `OUTPUT`: run report (kept / merged / dropped-prior-run counts, ledger stats).
- `MEMORY`: the aggregate to pass back next run.
- `AUDIT`: per-record decisions (first 1,000).

### Pricing (pay-per-event)

| Event | What you pay for | Price |
| --- | --- | --- |
| `record-processed` | Per place record processed | $1.00 per 1,000 |
| `apify-actor-start` | Run start, per GB of run memory | $0.005 |

**Worked examples:**

| Places in | Dedupe cost | Why it pays |
| --- | --- | --- |
| 5,000 | $5.01 | A documented community run had 5,000 leads with only ~700 unique places. Enriching raw at $0.10/lead: $500. Enriching deduped: $70. **$425 net saved on one run.** |
| 50,000 | $50.01 | At a typical 30% duplicate rate, you avoid enriching 15,000 repeats: $1,500 saved before deliverability damage is counted. |
| 500,000 | $500.01 | Streaming, chunked, no browser, no proxy — cost scales linearly and nothing else does at this volume. |

Set a max charge on any run (`Maximum cost per run` in Console, or
`ACTOR_MAX_TOTAL_CHARGE_USD` via API) and the actor stops cleanly at your budget.

### What the audit looks like on real data

From a real Chicago coffee-shop scrape (96 places in, 94 kept):

| Decision | Records | Reason |
| --- | --- | --- |
| merged | Swedish American Museum → its Museum Café | exact phone match, names agree (same organization) |
| merged | Jewel-Osco → Jewel-Osco Deli | exact phone match, names agree (same store) |
| **kept + flagged** | Sweethearts (shares phone with Lonesome Rose) | shared line but dissimilar names — a venue that changed hands is NOT silently merged |
| **kept** | PrintWithMe Kiosk *at* Eli Tea Bar | fuzzy name match vetoed by conflicting phone — a kiosk inside a venue is not the venue |

Two independent scrapes of the same neighborhood minutes apart overlapped ~90%; with
the memory passed between runs, the second run returned exactly the 10 genuinely new
places and dropped all 87 repeats.

### Integrations

- **API:** `POST https://api.apify.com/v2/acts/broomwagon~google-maps-deduper/runs`
  with `{"datasetId": "<scraper run's dataset>", "memory": <last MEMORY record>}` in
  the JSON body. Read `MEMORY` back from the run's key-value store and store it for
  the next call.
- **Apify Schedule (zero glue):** create a key-value store once (Storage → Key-value
  stores → Create), put its ID in `memoryStoreId`, and schedule the actor after your
  scraper. Runs chain themselves; each delivers only new places.
- **n8n / Make:** run the scraper node, pass its `defaultDatasetId` into this actor's
  `datasetId`, and map the previous execution's `MEMORY` output into `memory` — or
  skip the mapping entirely by using `memoryStoreId` as above.
- **MCP / AI agents:** callable as a tool via the Apify MCP server; agents round-trip
  the `MEMORY` object as tool context between calls.

### The Broomwagon family

This actor is one of nine deterministic post-processing tools from [Broomwagon](https://apify.com/broomwagon): the layer that follows your scrapers and agents, cleaning and watching what they produce. Same input, same output, every time.

- [Scraper Output Monitor](https://apify.com/broomwagon/scraper-output-monitor) — catch the day your scrape silently breaks.
- [Only New Items](https://apify.com/broomwagon/only-new-items) — deliver only records you have never delivered before.
- [Dataset Deduper](https://apify.com/broomwagon/dataset-deduper) — exact and fuzzy dedupe for any dataset, with an audit trail.
- [CSV Doctor](https://apify.com/broomwagon/csv-doctor) — repair broken CSV files, with a report of every fix.
- [LLM Output Guard](https://apify.com/broomwagon/llm-output-guard) — validate LLM and agent JSON against your schema.
- [PII Redactor](https://apify.com/broomwagon/pii-redactor) — strip emails, phones, SSNs, cards, and addresses, deterministically.
- [Record Linker](https://apify.com/broomwagon/record-linker) — fuzzy join two datasets that share no key.
- [AI Search Visibility Monitor](https://apify.com/broomwagon/ai-search-visibility-monitor) — track your brand's share of voice in AI answers.

# Actor input Schema

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

Output dataset of a Google Maps scraper run (compass/crawler-google-places, compass/google-maps-extractor, and similar all work). Pipe it straight in.

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

Place records to dedupe, provided inline as a JSON array. Handy for small jobs and testing.

## `fileUrl` (type: `string`):

URL of a JSON or NDJSON file of place records.

## `memory` (type: `object`):

An object beginning with "memoryVersion", copied from the MEMORY record in your last run's key-value store (not your data — that goes in Dataset or Inline items). Enables cross-run dedupe: places already delivered in earlier runs are dropped, so you only get what's new.

## `memoryStoreId` (type: `string`):

Optional: a key-value store in YOUR account. The actor reads the memory from it before running and writes the update back after, so scheduled runs chain automatically with no workflow tool.

## `fuzzyThreshold` (type: `integer`):

Minimum name similarity (Jaro-Winkler, 0-100) for two places without a shared placeId or phone to merge. Address similarity must also clear threshold minus 5.

## `ledgerCapacity` (type: `integer`):

Expected total unique places across ALL runs of this pipeline. Sizes the cross-run ledger (1% false-positive target at capacity).

## Actor input object example

```json
{
  "items": [
    {
      "title": "Kopi Cafe",
      "address": "5317 N Clark St, Chicago",
      "phone": "(773) 989-5674"
    },
    {
      "title": "Kopi Cafe & Boutique",
      "address": "5317 N Clark St, Chicago",
      "phone": "+1 773-989-5674"
    },
    {
      "title": "La Colombe",
      "address": "5158 N Clark St, Chicago",
      "phone": "(773) 293-6048"
    }
  ],
  "fuzzyThreshold": 92,
  "ledgerCapacity": 100000
}
```

# Actor output Schema

## `deduplicatedPlaces` (type: `string`):

Your places with duplicates removed, original records untouched.

## `memory` (type: `string`):

The compact aggregate holding the cross-run ledger. Send it back as `memory` on the next run, or skip this by using `memoryStoreId`.

## `report` (type: `string`):

Kept / merged / dropped-prior-run counts and ledger statistics.

## `audit` (type: `string`):

Per-record decisions with match reasons and scores (first 1,000).

# 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": "Kopi Cafe",
            "address": "5317 N Clark St, Chicago",
            "phone": "(773) 989-5674"
        },
        {
            "title": "Kopi Cafe & Boutique",
            "address": "5317 N Clark St, Chicago",
            "phone": "+1 773-989-5674"
        },
        {
            "title": "La Colombe",
            "address": "5158 N Clark St, Chicago",
            "phone": "(773) 293-6048"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("broomwagon/google-maps-deduper").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": "Kopi Cafe",
            "address": "5317 N Clark St, Chicago",
            "phone": "(773) 989-5674",
        },
        {
            "title": "Kopi Cafe & Boutique",
            "address": "5317 N Clark St, Chicago",
            "phone": "+1 773-989-5674",
        },
        {
            "title": "La Colombe",
            "address": "5158 N Clark St, Chicago",
            "phone": "(773) 293-6048",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("broomwagon/google-maps-deduper").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": "Kopi Cafe",
      "address": "5317 N Clark St, Chicago",
      "phone": "(773) 989-5674"
    },
    {
      "title": "Kopi Cafe & Boutique",
      "address": "5317 N Clark St, Chicago",
      "phone": "+1 773-989-5674"
    },
    {
      "title": "La Colombe",
      "address": "5158 N Clark St, Chicago",
      "phone": "(773) 293-6048"
    }
  ]
}' |
apify call broomwagon/google-maps-deduper --silent --output-dataset

```

## MCP server setup

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

```

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/FTIOHARMhT07PDOQb/builds/yc6B6rzLe9MF8t7Td/openapi.json
