Query file in, outreach list out

The pipeline is two commands. The first drives a headless browser over a list of search queries and writes a 36-column CSV; the second merges, deduplicates and filters those CSVs into four outreach lists and one rejection log. Everything below is what had to be learned to make those two commands produce something worth sending to.

Stage one is the scrape. The input is a plain text file of Google Maps search queries — one per line, of the form "cable assembly in Phoenix AZ" — crossed across the US metros worth covering. Twenty-four queries per vertical turned out to be the point of diminishing returns: past that, the same businesses start reappearing under different search terms and the deduplication step absorbs them.

Concurrency is capped at two, and this is the single most expensive thing to get wrong. Google throttles aggressively and silently. Measured on identical query sets: at four concurrent workers the run produced seven HTTP 429 responses and 47 failed jobs across ten queries. At two, there were fewer, and the scraper’s retry logic absorbed them. Raising concurrency does not make the run faster — it makes the run shorter by dropping results, which is a different thing and much harder to notice, because the output CSV looks perfectly healthy. It is just missing rows nobody counted. The only real fix is a rotating proxy pool, which costs money, and the throughput was never the constraint.

Stage two of the scrape is the part that justifies the whole exercise: an email crawl. For every business with a website, the scraper fetches that site and extracts email addresses from it. This is most of the runtime — far slower than the Maps traversal itself — and it is the reason the dataset has 251 verified addresses in it. No official API offers this. Google’s Places API returns a phone number and a website URL and stops there, so a pipeline built on it produces a call list, not an email list.

Stage three is the filter, a single Python script. It merges the raw CSVs per vertical, deduplicates on place_id — Google’s own stable identifier, which survives a business renaming itself or moving — and falls back to a normalised name-plus-address key for the rows where place_id is missing. Then it applies the category rules, validates the email column, and splits the survivors four ways: makers with an email, makers with only a phone or website, once per vertical. Rejections are not discarded. They are written to their own CSV with the reason attached, because a filter you cannot audit is a filter you will eventually stop trusting.

The whole run is idempotent and cheap to repeat. Raw CSVs are kept, so re-running the filter after changing a category rule takes seconds and does not touch Google at all. That matters more than it sounds: it means tuning the allowlist is a fast loop rather than an overnight one, and a fast loop is why the allowlist is any good.

Frequently asked questions

What concurrency should I use scraping Google Maps?

Two. Measured on identical query sets, four concurrent workers produced seven HTTP 429 responses and 47 failed jobs across ten queries; two produced far fewer and the retry logic absorbed them. Higher concurrency drops results silently rather than going faster.

How do you get email addresses from Google Maps?

You do not — Maps has no email field. The scraper visits each business’s own website and extracts addresses from it. That crawl is most of the runtime and is the only reason the dataset has emails at all.

How do you deduplicate scraped business listings?

On place_id, Google’s stable per-location identifier, which survives a rename or a move. Rows missing it fall back to a normalised name plus the first 40 characters of the address, which is enough to catch the overlap between two query sets.

How long does a scrape take?

Hours rather than minutes for 24 queries, and the Maps traversal is not the slow part — the per-website email crawl is. Budget by the number of businesses with websites, not by the number of queries.

Do you need proxies?

Not at concurrency two. A rotating proxy pool is the only way to go meaningfully faster, and the scraper supports one, but it is a paid dependency bought to solve a throughput problem this pipeline does not have.