Google Maps is the default place people go for local leads, and scraping it is the default way people try to get them out. That route comes with proxies, CAPTCHAs, broken selectors and search results that stop at around 120 places, which is why a 'scrape every plumber in Leeds' job often turns into many overlapping searches stitched together.
There is another source for the same core fields. The Overture Maps Foundation publishes an open places dataset of 80M+ businesses worldwide, refreshed monthly and built from Meta, Microsoft, Foursquare, AllThePlaces, BrightQuery and others, under permissive licences that allow storage and resale with attribution. What it does not have is Google's review layer: no star ratings, review counts, opening hours or photos.
This guide walks through building a usable lead list from that data: finding the right category, defining the area, filtering to contactable businesses, running several cities and merging them, and adding Facebook Page links where you need a social touchpoint.
Step by step
Decide which lead fields you really need
List the columns your outreach depends on. If it is name, category, address, phone, website and a role email such as info@, open data covers it. If you qualify leads by star rating or review count, or need opening hours, this approach will not give you those and a Google Maps scraper remains the right tool for that part.
Find the right category ids
Plain words such as dentist, plumber, lawyer, coffee or hotel are mapped to Overture's taxonomy for you, and a parent category includes its children, so restaurant also returns italian_restaurant. For anything less common, run the actor once with listCategories on. It returns the category list with ids, parents and worldwide place counts, filtered by the Name contains text, and it is free.
{ "listCategories": true, "keyword": "dent" }Define the search area
A city name is enough for most jobs; 32,000+ cities are built in, and countryCode picks the right one when a name exists in several countries. For a tighter catchment, give lat, lon and radiusKm, and results come back nearest first with a distanceKm field. A bounding box works too, up to 1.5 x 1.5 degrees, which is about one metro area.
{ "lat": 53.8008, "lon": -1.5491, "radiusKm": 8, "categories": ["plumber"], "maxPlaces": 500 }Filter to businesses you can actually contact
Turn on requireWebsite, requirePhone or requireEmail so that places without those fields are skipped; skipped places are never charged. Keep minConfidence at the default 0.5 to drop most stale or duplicate records, or raise it to 0.9 if you only want well-corroborated places. Personal-looking emails are removed automatically and counted in droppedPersonalEmails.
Run several cities and merge the results
Each run covers one area, so for a region you loop over cities. The script below runs one city at a time with the official Apify Python client, deduplicates on the place id and writes a CSV with the first phone, website and email for each business. Field values that are lists are joined so the CSV stays one row per place.
import csv import os from apify_client import ApifyClient client = ApifyClient(os.environ["APIFY_TOKEN"]) cities = ["Austin, TX", "Dallas, TX", "Houston, TX", "San Antonio, TX", "Fort Worth, TX"] def first(value): if isinstance(value, list): return value[0] if value else "" return value or "" def joined(value): if isinstance(value, list): return "; ".join(str(v) for v in value) return value or "" places = {} for city in cities: run = client.actor("actor-forge/business-places-data").call(run_input={ "location": city, "countryCode": "US", "categories": ["dentist"], "requireWebsite": True, "maxPlaces": 300, }) for item in client.dataset(run["defaultDatasetId"]).iterate_items(): places[item["id"]] = item with open("dentists_texas.csv", "w", newline="", encoding="utf-8") as f: writer = csv.writer(f) writer.writerow(["name", "fullAddress", "phone", "website", "email", "confidence", "attribution"]) for p in places.values(): writer.writerow([ p.get("name"), p.get("fullAddress"), first(p.get("phones")), first(p.get("websites")), first(p.get("emails")), p.get("confidence"), joined(p.get("attribution")), ]) print(f"{len(places)} unique places written")Add Facebook Page links where you need them
If your outreach includes social, run the same area through Facebook Pages by City. It returns only businesses whose Overture record links a Facebook Page, with the normalised page URL and numeric page id alongside the same contact fields. Its cap is maxResults rather than maxPlaces. You can join the two datasets on website or phone.
{ "location": "Austin, TX", "categories": ["dentist"], "maxResults": 300, "minConfidence": 0.6 }Keep attribution and use the contacts lawfully
The licences (CDLA-Permissive-2.0, Apache-2.0, CC0) let you store, enrich and resell the rows, as long as the attribution and licence texts travel with them, so keep the attribution field if you republish. Phones and emails are business contacts as published, but you are still responsible for how you contact people under GDPR, CAN-SPAM and similar laws.
Sample input
{
"location": "Austin, TX",
"categories": [
"dentist"
],
"maxPlaces": 200,
"requireWebsite": true,
"countryCode": "US"
}Sample output
A city-plus-category run returns one row per business like these:
| name | category | fullAddress | phones | websites | emails | confidence | licence |
|---|---|---|---|---|---|---|---|
| Bowcutt Dental | dental_clinic | 2501 S Lakeline Blvd, Cedar Park, TX 78613, US | +15123796571 | https://bowcuttdental.com/ | info@bowcuttdental.com | 1 | CDLA-Permissive-2.0 |
| Pecan Park Family Dentistry | dental_clinic | 10601 Pecan Park Blvd, Austin, TX 78750, US | +15122502424 | http://www.pecanparksmiles.com | info@pecanparksmiles.com | 0.9722 | Apache-2.0 AND CDLA-Permissive-2.0 |
| Serenity Dental | dental_clinic | 8731 State Hwy 71 #102, Austin, TX 78735, US | +15123875799 | https://serenitydentalaustin.com/ | info@serenitydentalaustin.com | 1 | CDLA-Permissive-2.0 |
| Kelly W. Keith, DDS | dental_clinic | 3800 Speedway, Austin, TX 78751, US | +15127954082 | 1 | CDLA-Permissive-2.0 |
Run it from code
cURL
curl -X POST "https://api.apify.com/v2/acts/actor-forge~business-places-data/run-sync-get-dataset-items" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"location": "Austin, TX", "categories": ["dentist"], "maxPlaces": 200, "requireWebsite": true, "countryCode": "US"}'JavaScript (Node.js)
// Node.js 18+ (built-in fetch). Set APIFY_TOKEN in your environment.
const input = {
"location": "Austin, TX",
"categories": [
"dentist"
],
"maxPlaces": 200,
"requireWebsite": true,
"countryCode": "US"
};
const res = await fetch(
"https://api.apify.com/v2/acts/actor-forge~business-places-data/run-sync-get-dataset-items",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.APIFY_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify(input),
}
);
const items = await res.json();
console.log(items.length, items[0]);Python
# pip install apify-client (set APIFY_TOKEN in your environment)
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run_input = {
"location": "Austin, TX",
"categories": [
"dentist"
],
"maxPlaces": 200,
"requireWebsite": True,
"countryCode": "US"
}
run = client.actor("actor-forge/business-places-data").call(run_input=run_input)
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
print(item)The run-sync-get-dataset-items endpoint waits for the run (up to 5 minutes) and returns the dataset rows directly. For longer runs, start the run asynchronously and read the dataset when it finishes, as the Python example does. Replace APIFY_TOKEN with your own token from the Apify console.
What it costs
Five Texas cities at 300 dentists each cost 5 x $0.003 in area queries plus 1,500 x $0.001 in places, so $1.515 in total. Adding 300 Facebook Pages for one of those cities costs $0.003 + 300 x $0.004 = $1.203.
| Charged for | When | Price | Per 1,000 | Worked example |
|---|---|---|---|---|
| Area query | once per run, when the area query returns at least one place | $0.003 | $3 | Any city, bbox or radius = $0.003; a query with no matching places is free |
| Place result | Charged per place row delivered (contacts, address, coordinates, sources and attribution included) | $0.001 | $1 | 200 dentists in Austin = $0.003 + $0.20 = $0.203; 1,000 restaurants in Berlin = $1.003 |
| Actor Start (platform start fee) | Charged when the Actor starts running. Number of events charged depends on Actor memory (one event per GB, minimum one event). | $0.00005 | — | One run at 1 GB = $0.00005 |
Ready to try it? Business Places Data — Google Maps Alternative, 80M+ POIs runs on Apify, pay per result.
Run on Apify → Read the full actor pageTips and pitfalls
- One run covers one area of up to about one metro area. For a county or country, loop over cities rather than drawing a huge box.
- Email coverage varies a lot by place and trade: in testing about 75% of Austin dentists had an email but only about 32% of Berlin restaurants. Use requireEmail only if email is your sole channel.
- operatingStatus is often empty, so a closed business can still appear until a source drops it. Raise minConfidence if bounced calls are costly for you.
- Smaller towns outside the built-in city list need a region or country in the location, for example "Dripping Springs, TX".
- Use includeFields to keep the dataset small if you only need a few columns, but keep attribution if the data will be shown to anyone else.
FAQ
Is open places data as complete as Google Maps?
Not everywhere. Coverage is dense in North America and Europe and thinner in rural areas and parts of Africa and Asia. In the test cities almost every dentist and restaurant had a phone and a website.
Can I get more than 120 results for one search?
Yes. There is no per-search cap; one run returns up to 20,000 places for an area, ordered by confidence or by distance for a radius search.
How do I find businesses that have no website?
Leave requireWebsite off and filter the output for rows with an empty websites field. Those are often good prospects for web design or marketing services.
Do I need a proxy or a Google API key?
No. The actor reads the public Overture GeoParquet files and never calls Google, so no proxies, keys or CAPTCHAs are involved.
Actors used in this guide
Business Places Data — Google Maps Alternative, 80M+ POIs
Business leads by city from open Overture Maps data: name, category, address, lat/lon, phone, website, email, socials and brand for 80M+ places…
$3 per 1,000 · area queryFacebook Pages by City — Open Data, Not Affiliated with Meta
Facebook Page leads by city: page URL and ID, name, category, address, lat/lon, phone, website, email and other socials, from Overture Maps open…
$3 per 1,000 · area queryRelated guides
How to Export UK Company Directors from Companies House in Bulk
Give Companies House Enrichment a list of company numbers or a SIC code, status and location filter, keep includeOfficers on, then flatten the…
How to Use UK Planning Applications as Sales Leads
Pull each council's weekly list of newly validated applications with UK Planning Applications, filter the proposal text for the work you do, and…
How to Find a Company's Job Board and ATS (Workday, Greenhouse, Lever, Ashby)
Open any job on the company's careers page and look at the host: myworkdayjobs.com, greenhouse.io, lever.co, ashbyhq.com and similar tell you the…