Google Maps Scraper: Places, Leads, Emails avatar

Google Maps Scraper: Places, Leads, Emails

Pricing

from $1.20 / 1,000 places

Go to Apify Store
Google Maps Scraper: Places, Leads, Emails

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

Snow Leo Data

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

an hour ago

Last modified

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:

ColumnSearch answer onlyWith the detail card (default)
title, address, coordinates, city, postcode, category100%100%
rating100%100%
opening hours97.5%97.5%
attributes91.2%91.2%
phone in E.16489.4%89.4%
website86.2%86.2%
reviews count50.0%83.1%
plus code0%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: auto
, and it is included in the price — the reference Actors in this niche bill the same thing as a separate place-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:

DepthAreas at mostMeasured on coffee in Austin
01160 places, 8 requests, 9 s
14505 places, 40 requests, 19 s
216766 places, 168 requests, 42 s
364
4256

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. nameContains here 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: without
turns exactly that gap into a lead list.

Why 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.