Google Maps Scraper Deduper | Merge Duplicates | Cross-Run avatar

Google Maps Scraper Deduper | Merge Duplicates | Cross-Run

Pricing

from $1.00 / 1,000 record processeds

Go to Apify Store
Google Maps Scraper Deduper | Merge Duplicates | Cross-Run

Google Maps Scraper Deduper | Merge Duplicates | Cross-Run

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.

Pricing

from $1.00 / 1,000 record processeds

Rating

0.0

(0)

Developer

Brandon Mensing

Brandon Mensing

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

4 days ago

Last modified

Share

Google Maps Scraper Deduper

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:

[{"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:

[{"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)

EventWhat you pay forPrice
record-processedPer place record processed$1.00 per 1,000
apify-actor-startRun start, per GB of run memory$0.005

Worked examples:

Places inDedupe costWhy it pays
5,000$5.01A 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.01At a typical 30% duplicate rate, you avoid enriching 15,000 repeats: $1,500 saved before deliverability damage is counted.
500,000$500.01Streaming, 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):

DecisionRecordsReason
mergedSwedish American Museum → its Museum Caféexact phone match, names agree (same organization)
mergedJewel-Osco → Jewel-Osco Deliexact phone match, names agree (same store)
kept + flaggedSweethearts (shares phone with Lonesome Rose)shared line but dissimilar names — a venue that changed hands is NOT silently merged
keptPrintWithMe Kiosk at Eli Tea Barfuzzy 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: the layer that follows your scrapers and agents, cleaning and watching what they produce. Same input, same output, every time.