Google Maps Scraper: Places, Leads, Emails
Pricing
from $1.20 / 1,000 places
Google Maps Scraper: Places, Leads, Emails
Extract data from thousands of Google Maps locations and businesses: names, addresses, phones, websites, categories, opening hours, images, ratings and review counts, plus contact info - emails and social profiles. Export data, run via API, schedule and monitor runs, or integrate with other tools.
Pricing
from $1.20 / 1,000 places
Rating
0.0
(0)
Developer
Snow Leo Data
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
an hour ago
Last modified
Categories
Share
Google Maps Scraper: places, business leads and emails
Google Maps answers about 150 places for one search term in one map view and then goes quiet — page seven comes back empty, whatever you ask. That is the ceiling every Google Maps scraper hits, and it is the reason a "coffee in Austin" export stops at a hundred and fifty rows while the city holds thousands of cafes.
This Actor breaks that ceiling by splitting the map. It searches your area,
notices which parts came back full, cuts those parts into four and searches
each of them, and keeps going until the corners stop being crowded. Measured
on coffee in Austin, Texas on 12 September 2026: 160 places with no
splitting, 766 places at grid depth 2 — 4.79 times more, in 42 seconds and
168 requests. The run is reproducible with python3 tools/measure_ceiling.py
and the raw output is kept in tools/measurements.json.
Everything is plain HTTPS from the Apify data centre. No proxies, no browser, no API key, nothing to configure.
What you get for one place
One row per place, 56 columns. The identity block is place_id, cid, fid
and kgmid — four different ids because four different tools want four
different ones, and converting between them afterwards is a chore.
Then: title, subtitle, description, category and the full
categories list, the one-line address plus street, city,
postal_code, state, country_code and neighborhood split apart,
plus_code, latitude, longitude, phone as Google prints it and
phone_e164 ready for a dialler or a CRM, website and its bare domain,
rating, reviews_count and reviews_url, opening_hours as a structured
week with numeric open/close times, the same week as one readable
opening_hours_text line, additional_opening_hours for separate lunch or
brunch schedules, open_now, permanently_closed, timezone, owner_name,
attributes (accessibility, service options, amenities, payment — grouped,
not a flat heap of strings), image_urls and images_count, booking_links,
maps_url, search_page_url, and the bookkeeping columns search_term,
search_area, rank_in_search, language, source, change_type and
scraped_at.
Switch on the website enrichment and seven more arrive: emails,
email_count, social_profiles, social_platforms, social_count,
website_title, website_platform, plus website_status,
website_pages_crawled and website_error so you can see what happened to
each site.
How full those columns actually are
Measured on 160 places across four cuts — coffee in Austin, dentist in
Manchester, bakery in Lyon and hardware store in Round Rock — and stored
in tools/completeness.json:
| Column | Search answer only | With the detail card (default) |
|---|---|---|
| title, address, coordinates, city, postcode, category | 100% | 100% |
| rating | 100% | 100% |
| opening hours | 97.5% | 97.5% |
| attributes | 91.2% | 91.2% |
| phone in E.164 | 89.4% | 89.4% |
| website | 86.2% | 86.2% |
| reviews count | 50.0% | 83.1% |
| plus code | 0% | 100% |
That gap is the whole reason the detail card exists. Google's search answer is not stable: the same place, asked twice within a minute, comes back once with the review count and once without. We measured the share of places carrying a review count swinging between 37% and 100% over an hour on the same query. So the Actor tops up incomplete rows from the place card and, if the card also comes back thin, asks it one more time. That is
placeDetails: autoplace-details-scraped event.
Telling the Actor where to look
Three ways, pick one:
Put what you are looking for into searchTerms, one term per line, then
pick the area.
A place name. Type Austin, Texas, Bavaria, Portugal or
10115 Berlin into Location. Google resolves it to the real
administrative boundary and the Actor searches inside that boundary — for
New York it returns 40.4766..40.9177 by -74.2588..-73.7002, which is the
city's actual outline, not a circle around Times Square.
A name Google does not recognise is refused, not guessed. That matters
more than it sounds: on a made-up name the source does not return an error,
it hands your own request back unchanged, and a naive scraper reads that as
"search the entire planet". Type Zzqqxx Notaplace here and you get an empty
run with the reason location-not-understood and no place request sent at
all.
A point and a radius. latitude, longitude, radiusKm. The rectangle
is built with the cosine of the latitude, so 10 km is 10 km in Oslo as well
as in Nairobi.
An exact rectangle. boundingBox as
{"south": …, "west": …, "north": …, "east": …}Grid depth, and what it costs
gridDepth is how many times an area may be cut in four:
| Depth | Areas at most | Measured on coffee in Austin |
|---|---|---|
| 0 | 1 | 160 places, 8 requests, 9 s |
| 1 | 4 | 505 places, 40 requests, 19 s |
| 2 | 16 | 766 places, 168 requests, 42 s |
| 3 | 64 | — |
| 4 | 256 | — |
Only the areas that hit the wall are cut. An area that returned thirty places out of a possible hundred and fifty holds everything there is, and splitting it would be four requests spent on nothing. Depth 2 is the default and is a reasonable answer for a city.
Number of places means delivered places
maxPlaces counts rows that reach your dataset, not rows that were crawled.
This is not a detail. The usual way this goes wrong elsewhere: you ask for 30
places in the Construction category, the crawl stops after 30 places of any
category, the filter removes all 30, the run finishes green and the dataset is
empty. Here the crawl keeps going until your order is filled, and a per-term
quota (maxPlacesPerTerm) stops one popular term from eating the whole
budget before the next one starts.
Filters never empty your dataset by accident
Every filter follows one rule: a missing field is not a mismatch. If you filter on a minimum rating and a place arrives without a rating, the Actor fetches its detail card first and only then decides. If the card has no rating either — a brand new business with no reviews — the row passes rather than vanishing.
Available filters: categoryContains, minRating, minReviews,
maxReviews, websiteFilter (any / only with a website / only without),
onlyWithPhone, onlyOpenNow, skipPermanentlyClosed, nameContains,
excludeContains, onlyCountries, onlyWithEmail and
onlyCorporateEmail. The REPORT record at the end names which filter dropped
how many rows, so a thin result is never a mystery.
Only places without a website is the classic lead list: those are the
businesses that buy one. It pairs with minReviews to skip the dead
listings.
Emails and social profiles
Google Maps has no email field at all — it never did. Switch on
enrichFromWebsite and the Actor opens the business's own site, follows its
contact and imprint links, and reads out every address and social profile it
publishes: mailto: links, addresses written as text, schema.org JSON-LD,
and addresses that Cloudflare has obfuscated in the markup. Up to
websitePagesPerPlace pages per site, 3 by default.
A site that answers 403, or has quietly expired, does not remove the place
from your results. The row arrives with website_status and
website_error filled in, so the address can be marked dead in your own
database instead of silently disappearing from the export.
onlyCorporateEmail drops places whose only address is on Gmail, Yahoo,
Hotmail, GMX, Mail.ru and the like.
Watching places change over time
Switch on onlyChanged and the Actor remembers, between runs, what it has
already delivered. On the next run it returns only what is new or what moved:
a rating that went up, a review count that grew, a phone number that changed,
a website that appeared, an address that was corrected, opening hours that
were edited, or a place that closed for good. Every row carries
change_type: NEW, UPDATED or UNCHANGED.
The fingerprint deliberately ignores photos and booking links: Google reshuffles those on every request and a monitor built on them would report everything as changed, every day.
It also treats a missing field as "no news", not as a change. This is the
difference between a usable monitor and an expensive one. Measured in the
cloud before the fix: two runs over the same town forty seconds apart reported
89 of 92 bakeries as UPDATED, purely because Google had dropped their ratings
and trimmed their opening hours from the second answer. After the fix the same
three runs in a row returned 160 unchanged, then one genuinely new bakery, then
nothing - and said nothing-changed each quiet time. Opening hours only count
towards a change when a full seven-day week came back.
This is the mode the market actually runs in. The reference review scraper in this niche shows 162 554 118 runs against 56 060 users — 2 900 runs per user, which is not one-off exporting, it is a schedule. Memory lives in a named key-value store, so it survives between runs; the default store is wiped at the start of every run and would have quietly turned monitor mode into a plain re-export.
An empty result always says why
A green run with nothing in it is the worst thing a paid Actor can do. This
one never does it silently. The REPORT record carries empty_reason, one of
ten named values, and the log carries a sentence in plain English.
Six of them are decided before a single byte is fetched, because they are decided by your input alone:
no-search-given— no search term and no place id.location-not-understood— Google does not know that place name.email-filter-without-enrichment— you asked for places with an email but left the website enrichment off, and Maps itself carries no email, so every row would be dropped.rating-filter-impossible— a minimum rating above 5.0.website-filter-contradicts-email— places without a website and with an email at the same time; the email is read from the website.charge-limit-reached— the run's charge limit is already spent.
Four are only visible afterwards: source-returned-nothing, filtered-out,
nothing-changed (monitor mode working exactly as asked) and
nothing-collected.
Looking up places you already know
Paste ids into placeIds and skip the search entirely. Four formats are
accepted: a Google place id (ChIJ…), a numeric CID, an internal feature id
(0x…:0x…), and a plain Google Maps link with any of those in it. Anything
that is none of those is listed by name in ignored_input rather than
silently thrown away — a postcode typed into that box is not mistaken for a
CID.
Output shape
compactOutput cuts the row to 16 columns — name, where, how to contact,
how good — which is what an AI agent or a CSV import usually wants.
dropEmptyFields leaves out every column that has no value in that row.
Column order is fixed and does not depend on what arrived first, so a
scheduled export keeps the same CSV header from week to week.
Honest limitations
The reference Actor in this niche publishes 99 dataset fields; 58 of them have an equivalent here. These do not, and were looked for in live payloads on 12 September 2026 rather than assumed absent:
- review text. This Actor gives you the rating, the review count and the link to the reviews page. It does not pull individual reviews with author, date and text.
- price band (
$/$$/$$$), popular times, review distribution by star, review keyword tags, questions and answers, owner updates, "people also search for", web results. - hotel, restaurant and fuel verticals: star ratings, hotel ads, reservation providers, online ordering widgets, menus, fuel prices.
- decision-maker leads and email verification. The reference sells both as $0.10-per-record add-ons backed by a third-party database. This Actor reads only what a business publishes on its own website.
- photos beyond the first few, and image authors.
- scraping inside a mall or shopping centre, and exact-name matching.
nameContainshere is a substring filter. - The search area is a rectangle or a circle, not a GeoJSON polygon.
A full line-by-line table, including the billing comparison, is in
COMPARISON.md.
FAQ
Do I need a Google API key? No. Nothing is configured, nothing is billed by Google, and there is no quota to run out of.
Do I need proxies? No. The source was probed from inside the Apify cloud before a line of this Actor was written — twelve requests from six cities on four continents, all 200, all with data. That check exists because it is exactly where a scraper dies: a source that answers a laptop happily can refuse every data-centre address.
How is this different from the official Places API? Places API charges per call, caps a text search at 60 results, and needs a billing account. This Actor has no key, returns the same core fields, and goes past 150 per search by splitting the map.
Why do some places have no phone or website? Because the business never published one. Measured on 160 live places: 89.4% had a phone and 86.2% had a website. The remainder is real-world data, not a failure — and
websiteFilter: withoutWhy is the review count sometimes missing? Because Google leaves it out of
its own answer at random. The Actor asks the place card again when that
happens, which lifted coverage from 50.0% to 83.1% in our measurement. When
the count is genuinely absent everywhere, the field is null rather than a
guess.
Can I search several terms at once? Yes. Every term is searched across the
whole area. Use maxPlacesPerTerm if you want each term to get a fair share
of the budget.
Does it work outside the United States? Yes. The live test suite searches
Paris, Tokyo, Sydney and São Paulo on every run. Set language for the
language of names and categories, and countryCode for which Google front
end to ask.
What happens if a run is interrupted? Rows already pushed stay pushed, and nothing is marked as delivered until it actually has been. The next run owes you everything that did not arrive. This is covered by a test that cuts the push in half on purpose.
How do I run it every day? Set a schedule, switch on onlyChanged, and
read change_type. Quiet runs mean nothing moved, and they say so with
empty_reason: nothing-changed instead of leaving you guessing.
Is the data legal to collect? It is public business information that Google shows to anyone without a login. Treat personal data in it — an owner name, a personal mobile — under the rules that apply to you.