- Oct 18, 2026
- 3 min read
From Google Maps to a UK Lead List: Apify, Business Websites and Companies House
Once I’d decided that cold email made sense only to UK companies (why), the job became a filter: start from every business of one kind in one city, and end with the few that are (a) companies, (b) reachable at a business address, and (c) worth writing to.
Three steps, each a make target that writes a file, so any step can be
re-run alone.
make scrape Q="vet" L="Exeter, UK" MAX=100 # raw.json
make enrich RUN=data/<run> # leads.csv
make batch RUN=data/<run> ARGS="--limit=30" # batch/smartlead.csv + PDFs
Step 1: scrape the listings (and nothing else)
Apify’s Google Maps scraper (compass/crawler-google-places) takes a search
term and a location. The defaults collect far more than a lead list needs. An
earlier experiment of mine kept 4,000 Google reviews with reviewers’ names
for a few hundred businesses: personal data about private individuals, with
no use at all.
So the input switches that off, and only business fields are written:
run_input = {
"searchStringsArray": [query],
"locationQuery": location,
"language": "en",
"maxCrawledPlacesPerSearch": max_places,
"maxReviews": 0, # reviewers are private individuals
"maxImages": 0,
"skipClosedPlaces": True,
}
keep = ["title", "website", "phone", "city", "postalCode", "countryCode", "categoryName"]
places = [{k: it.get(k) for k in keep} for it in items if not it.get("permanentlyClosed")]
One gotcha: apify-client 3.x returns a Run object rather than a dict
(run.default_dataset_id). My first run finished on Apify and then crashed
reading the result. If that happens, fetch the actor’s last_run() instead of
paying for a second run.
Step 2: enrich, from public sources only
Two questions per business:
- Is there a business email on its own website? Not from a paid enrichment database: from the homepage and contact pages of the business itself (how, in the next post).
- Is it a company? The UK register, Companies House, has a free API. An active Ltd, LLP or PLC can be emailed; sole traders can’t (matching without false positives, in a later post).
The output keeps the evidence next to the answer: the page the email came from, the registered name and number, and whether the postcode matched.
Step 3: audit, and only keep sites with something to say
Each remaining website goes through the same audit engine as my public audit tool: mobile speed, search basics, security, broken links. The batch picks one concrete finding per site and writes it as a plain sentence. Sites that are fine are not emailed: no finding, no pitch (the email it feeds is a later post).
A first run, in numbers
One niche, one UK city, 30 places:
| Stage | Count |
|---|---|
| Places found (all UK) | 30 |
| With a website | 26 |
| With an email on their own domain | 16 |
| Confirmed company (Companies House) or “Ltd” in the name | 7 |
| With a finding worth an email | 7 |
Thirty businesses became seven well-founded emails. Six of the seven sites took over 10 seconds to show their main content on a phone, which Google’s own PageSpeed test confirmed (why I double-check speed numbers is a later post).
What lives where
- Lead data stays in
data/<run>/, excluded from git, deleted after 90 days withmake clean-old. - Keys (
APIFY_TOKEN,CH_API_KEY) live in.env, readable only by me. - The niche, city and businesses never appear in content like this post.
Takeaways
- Filter towards fewer, better rows. Thirty in, seven out is a good result.
- Collect only what the email needs, starting at the scraper’s input.
- Keep the evidence for every decision: which page gave the email, which register entry made it a company.