Google Maps Scraper - Business Leads, Emails & Reviews
Pricing
Pay per usage
Google Maps Scraper - Business Leads, Emails & Reviews
Scrape Google Maps business data at scale: names, addresses, phones, websites, emails, opening hours, ratings and reviews. HTTP-first engine, 10-30x faster than browser scrapers, breaks the 120-result limit. Export to CSV, Excel, JSON.
Pricing
Pay per usage
Rating
0.0
(0)
Developer
Andrew Babo
Maintained by CommunityActor stats
0
Bookmarked
33
Total users
21
Monthly active users
a day ago
Last modified
Categories
Share
Google Maps Scraper — extract business data, leads and reviews
Scrape Google Maps business listings at scale and export them to CSV, Excel, JSON or the API. For any city, region, country or map area you get business names, full addresses, phone numbers, websites, emails, categories, opening hours, ratings, review counts, star histograms, photos and individual reviews.
Built on an HTTP-first engine that is ~10–30× faster than browser-based Google Maps scrapers, with adaptive anti-blocking and quadtree tiling that breaks Google's ~120-results-per-viewport limit. Cost is roughly $0.03 per 1,000 places.
What you can use it for
- Lead generation — build B2B prospect lists of local businesses with phone, website and email.
- Market and competitor research — map every competitor in an area with ratings and review volume.
- Review monitoring — pull reviews and star distribution for a brand or a whole category.
- Local SEO audits — check listings, categories, opening hours and contact data at scale.
- Data enrichment — hydrate place IDs, CIDs or Google Maps URLs into full records.
How to scrape Google Maps with this actor
- Enter one or more search terms in
searchStringsArray(e.g.["coffee shop", "dentist"]). - Set
locationQueryto a city, region or country — or passbbox,centerPoint+radiusMeters, or a GeoJSONcustomGeolocation. - Optionally set
maxCrawledPlacesPerSearch,maxReviews,maxImagesandscrapeContacts(emails + social links). - Run the actor and download the dataset as CSV, Excel, JSON, XML or read it through the Apify API.
FAQ
Can I scrape emails from Google Maps? Google Maps itself does not publish emails. With scrapeContacts: true the actor visits each business website and extracts emails[] and socialLinks{} from it.
Does it get more than 120 results? Yes. Google returns only ~120 places per viewport, so the actor tiles the area adaptively and deduplicates by placeId, returning thousands of places for a metro area.
Do I need proxies? The HTTP tier rarely needs residential proxies. Any Apify proxy group works; bandwidth stays tiny because no images or JavaScript are downloaded.
Is it free? The actor itself is free to use — you only pay Apify platform compute, around $0.03 per 1,000 places, so the free plan covers a large volume.
Is scraping Google Maps legal? This actor collects publicly visible business information. Review data contains personal data, so only enable maxReviews when you need it and comply with GDPR and Google's terms in your jurisdiction.
How it works
- HTTP tier (default) — calls the same internal JSON endpoints the Maps frontend uses (
/search?tbm=mapfor listings,/maps/preview/placefor details, and an internal BOQ proxy for reviews). No browser, no rendering: tens of ms per request. - Browser stealth tier (fallback) — when the HTTP tier detects a block (
/sorry/, consent wall, 429/403), the affected tile automatically re-runs through a stealth Playwright browser (engine: "auto"). - Adaptive quadtree tiling —
locationQuery(or a GeoJSONcustomGeolocation) is geocoded via OpenStreetMap, then the area is probed tile by tile; any tile returning a dense result set (≥18/20 per page) is split into 4 sub-tiles. This yields far more than Google's ~120-result cap while saving requests on sparse areas. - AIMD adaptive concurrency — concurrency grows while requests succeed and halves on every block signal, keeping block-rate near zero without fixed rate limits.
- Dedup by
placeId— overlapping tiles never produce duplicate rows.
Input
| Field | Type | Description |
|---|---|---|
searchStringsArray | string[] | Queries, e.g. ["coffee shop", "restaurant"] |
locationQuery | string | City/region/country, geocoded via OSM (e.g. "New York, USA") |
customGeolocation | GeoJSON | Point (5 km radius), Polygon or MultiPolygon — overrides locationQuery |
maxCrawledPlacesPerSearch | number | Cap per search string (0 = unlimited) |
language / countryCode | string | hl / gl, e.g. en / us (defaults) |
categoryFilterWords | string[] | Keep only places whose category contains any word |
placeMinimumStars | string | Keep only places with rating ≥ value |
skipClosedPlaces | boolean | Skip permanently closed places (default true) |
scrapePlaceDetailPage | boolean | Enrich with address/phone/website/hours (default true; false = listing-only, fastest) |
maxReviews | number | Reviews per place (0 = none), 10 per page |
reviewsSort | string | most_relevant | newest | highest_rating | lowest_rating |
reviewsStartDate | string | ISO date; older reviews skipped (with newest sort, stops early) |
maxImages | number | Max photo URLs per place (0 = none) |
startUrls | array | Direct Google Maps place URLs |
placeIds | string[] | Direct place IDs (ChIJ... or 0x...:0x...) |
engine | string | auto (default) | http | browser |
maxConcurrency | number | Upper bound for the adaptive controller |
failOnZeroResults | boolean | Fail the run when 0 places were pushed while blocks were detected (default true) — prevents silent-empty runs |
deepRecall | boolean | More aggressive tiling (lower split threshold, deeper quadtree) for maximum recall |
includeRunSummary | boolean | Push a final __runSummary item with counts and field coverage |
mode | string | ids (identifiers only) | basic (listing fields, no detail request) | full (default) |
bbox | number[] | [south, west, north, east] — explicit area, overrides locationQuery |
centerPoint + radiusMeters | number[] / number | Area around a point — [lat, lng], latitude first (a {lat,lng} object is also accepted through the API). Wrong order fails fast with INVALID_COORDINATES; a malformed bbox fails with INVALID_BBOX. Default radius 5000 m |
gridStepMeters | number | Seed grid cell size. Leave at 0 (default) to derive it from the area: one Google viewport only ever returns ~120 places, so metro/province/country areas are gridded automatically (and the grid is widened if it would exceed the request budget) |
cids | string[] | Decimal Google CIDs to hydrate into full place records |
scrapeContacts | boolean | Fetch each place website and extract emails + socialLinks (1 extra request per place) |
speedMode | string | turbo (default, fastest with full coverage) | fast | balanced | accurate |
scrapeReviewsDistribution | boolean | Rescue the star histogram from the review feed when the place payload omits it (default true) |
requestTimeoutSecs | number | Per-request timeout (default 30) |
maxRunTimeSecs | number | Hard wall-clock budget for the crawl (default 3600) |
Output
One dataset item per place (field names mirror compass/crawler-google-places for drop-in compatibility):
title, placeId, fid, cid, categoryName, categories[], address, street, city, state, postalCode, countryCode, location.lat/lng, plusCode, totalScore, reviewsCount, reviewsDistribution, openingHours[] ({day, hours}), website, phone, imageUrls[], reviews[] (reviewId, name, profilePhotoUrl, reviewerUrl, reviewerId, stars, publishedAtDate, text, textLanguage, likesCount, responseFromOwnerText, reviewImageUrls[]), url, searchString, rank, scrapedAt.
Every item on every branch (search, bbox, grid, cids, startUrls) carries the full compass field set — unknown values are null, never missing keys — plus categoryMatched (true/false with categoryFilterWords, otherwise null).
Every item also carries datasetSchemaVersion, detailLevel (ids/basic/full), matchedSearchStrings, language, searchCountryCode, and — with scrapeContacts — emails[] and socialLinks{}.
Error codes
The run fails with a machine-readable prefix in the status message and errorCode in STATS: NO_INPUT, INVALID_LOCATION, INVALID_COORDINATES, INVALID_BBOX, GEOCODE_FAILED, INVALID_CID, ALL_BLOCKED, NO_RESULTS, NO_RESULTS_IN_AREA.
Geo accuracy (0.8)
Google ignores a viewport that is too wide or covers a sparse area and answers from the egress IP instead, which used to leak places from the proxy's country into the dataset. The actor now:
- grids every area (tiles never exceed 15 km) and derives the zoom from the tile span instead of a fixed value;
- skips tiles that do not intersect the geocoded admin polygon;
- drops records outside the requested area at collection time, so the
maxCrawledPlacesPerSearchbudget is spent on real results; - reports
outOfArea,inAreaPctandipLeakPagesinSTATS/__runSummary, and fails withNO_RESULTS_IN_AREAwhen nothing inside the area survived.
includeRunSummary now defaults to true.
Cost
Measured on live runs: 500 places in 44 s on 4 GB, i.e. roughly $0.003 per 100 places ($0.03 per 1,000 places) of Apify compute at the default settings. mode: "basic" or scrapePlaceDetailPage: false cuts that roughly in half; maxReviews and scrapeContacts add one request per place.
Run statistics are stored in the key-value store under STATS: searched/listed/enriched/pushed/blocked counts, degraded flag, and per-field coverage percentages.
Data source: listing-first
The listing response already carries address parts, opening hours, E.164 phone, images, categories and coordinates, so the actor pushes listing records directly and only fetches the place detail page when a record is incomplete. Measured coverage on 160 live places (SG/VN/AU/MY): title 100%, placeId/cid 100%, location 100%, categoryName 100%, address 99%, city 99%, countryCode 100%, openingHours 96%, website 91%, phone/phoneUnformatted 94%, imageUrl 88%, postalCode 88%, totalScore 88%, state 74%.
Performance notes
- Listing page (20 places): ~0.5 s. Place detail: ~0.15 s. 10 reviews: ~0.6 s.
scrapePlaceDetailPage: falsegives the cheapest, fastest listing-only crawl.- Residential proxies are rarely needed for the HTTP tier; if you enable
proxyConfiguration, bandwidth stays tiny because no images/JS are downloaded. - For very large jobs, run several instances in parallel with different
locationQueryareas instead of one huge run.
Known limitations
- The internal
pbformats have no stability contract; parsers fail safe (null fields, not crashes) andengine: "auto"falls back to the browser if Google changes a response shape. - Reviews include reviewer profile data — only enable
maxReviewswhen you actually need it (personal data). - Partial runs are labelled: when
maxRunTimeSecsor the tile budget is reached, the run still succeeds butSTATS/__runSummarysetpartial: true,timedOut, anddegradedso a pipeline can tell a partial sweep from a complete one. reviewsDistribution(star histogram) is supported over plain HTTP since 0.9.16 and reaches 98-100% coverage since 1.5 (places with zero reviews get an exact all-zero histogram). The breakdown lives at[175][3]of the place record, but Google only serves it when thepbcarries the!14m2!7e81!15i10112group before!15m106(94 KB payload without it and[175] = null; 249 KB with it and the real[1★..5★]counts). No session token or browser is needed — verified against a live browser capture and reproduced from Apify IPs. When a record still comes back lean,scrapeReviewsDistribution: truederives the counts from the full review feed, and only when the whole feed was fetched.reviewsCountandplusCodeare best-effort: Google only serves them for a subset of records on the internal endpoints, so expect partial coverage. UsemaxReviewsif you need review data itself.openingHoursfrom the listing tier covers the current week's day entries; historical/special hours are not exposed.- Not affiliated with Google. You are responsible for complying with applicable terms and privacy laws (GDPR, ...).