Google Maps Scraper - Business Leads, Emails & Reviews avatar

Google Maps Scraper - Business Leads, Emails & Reviews

Pricing

Pay per usage

Go to Apify Store
Google Maps Scraper - Business Leads, Emails & Reviews

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

Andrew Babo

Maintained by Community

Actor stats

0

Bookmarked

33

Total users

21

Monthly active users

a day ago

Last modified

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

  1. Enter one or more search terms in searchStringsArray (e.g. ["coffee shop", "dentist"]).
  2. Set locationQuery to a city, region or country — or pass bbox, centerPoint + radiusMeters, or a GeoJSON customGeolocation.
  3. Optionally set maxCrawledPlacesPerSearch, maxReviews, maxImages and scrapeContacts (emails + social links).
  4. 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

  1. HTTP tier (default) — calls the same internal JSON endpoints the Maps frontend uses (/search?tbm=map for listings, /maps/preview/place for details, and an internal BOQ proxy for reviews). No browser, no rendering: tens of ms per request.
  2. 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").
  3. Adaptive quadtree tilinglocationQuery (or a GeoJSON customGeolocation) 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.
  4. AIMD adaptive concurrency — concurrency grows while requests succeed and halves on every block signal, keeping block-rate near zero without fixed rate limits.
  5. Dedup by placeId — overlapping tiles never produce duplicate rows.

Input

FieldTypeDescription
searchStringsArraystring[]Queries, e.g. ["coffee shop", "restaurant"]
locationQuerystringCity/region/country, geocoded via OSM (e.g. "New York, USA")
customGeolocationGeoJSONPoint (5 km radius), Polygon or MultiPolygon — overrides locationQuery
maxCrawledPlacesPerSearchnumberCap per search string (0 = unlimited)
language / countryCodestringhl / gl, e.g. en / us (defaults)
categoryFilterWordsstring[]Keep only places whose category contains any word
placeMinimumStarsstringKeep only places with rating ≥ value
skipClosedPlacesbooleanSkip permanently closed places (default true)
scrapePlaceDetailPagebooleanEnrich with address/phone/website/hours (default true; false = listing-only, fastest)
maxReviewsnumberReviews per place (0 = none), 10 per page
reviewsSortstringmost_relevant | newest | highest_rating | lowest_rating
reviewsStartDatestringISO date; older reviews skipped (with newest sort, stops early)
maxImagesnumberMax photo URLs per place (0 = none)
startUrlsarrayDirect Google Maps place URLs
placeIdsstring[]Direct place IDs (ChIJ... or 0x...:0x...)
enginestringauto (default) | http | browser
maxConcurrencynumberUpper bound for the adaptive controller
failOnZeroResultsbooleanFail the run when 0 places were pushed while blocks were detected (default true) — prevents silent-empty runs
deepRecallbooleanMore aggressive tiling (lower split threshold, deeper quadtree) for maximum recall
includeRunSummarybooleanPush a final __runSummary item with counts and field coverage
modestringids (identifiers only) | basic (listing fields, no detail request) | full (default)
bboxnumber[][south, west, north, east] — explicit area, overrides locationQuery
centerPoint + radiusMetersnumber[] / numberArea 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
gridStepMetersnumberSeed 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)
cidsstring[]Decimal Google CIDs to hydrate into full place records
scrapeContactsbooleanFetch each place website and extract emails + socialLinks (1 extra request per place)
speedModestringturbo (default, fastest with full coverage) | fast | balanced | accurate
scrapeReviewsDistributionbooleanRescue the star histogram from the review feed when the place payload omits it (default true)
requestTimeoutSecsnumberPer-request timeout (default 30)
maxRunTimeSecsnumberHard 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 scrapeContactsemails[] 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 maxCrawledPlacesPerSearch budget is spent on real results;
  • reports outOfArea, inAreaPct and ipLeakPages in STATS / __runSummary, and fails with NO_RESULTS_IN_AREA when 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: false gives 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 locationQuery areas instead of one huge run.

Known limitations

  • The internal pb formats have no stability contract; parsers fail safe (null fields, not crashes) and engine: "auto" falls back to the browser if Google changes a response shape.
  • Reviews include reviewer profile data — only enable maxReviews when you actually need it (personal data).
  • Partial runs are labelled: when maxRunTimeSecs or the tile budget is reached, the run still succeeds but STATS/__runSummary set partial: true, timedOut, and degraded so 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 the pb carries the !14m2!7e81!15i10112 group 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: true derives the counts from the full review feed, and only when the whole feed was fetched.
  • reviewsCount and plusCode are best-effort: Google only serves them for a subset of records on the internal endpoints, so expect partial coverage. Use maxReviews if you need review data itself.
  • openingHours from 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, ...).