Google Maps Scraper With Lead & Contact Enrichment
Pricing
Pay per usage
Google Maps Scraper With Lead & Contact Enrichment
No coding needed—just input a search query and let this scraper collect businesses from Google Maps. Export data as JSON or CSV. Great for no-code workflows, outreach planning, and local discovery tasks.
Pricing
Pay per usage
Rating
0.0
(0)
Developer
API Empire
Maintained by CommunityActor stats
0
Bookmarked
6
Total users
0
Monthly active users
6 days ago
Last modified
Categories
Share
Google Maps Scraper — Places, Emails, Phones and Social Profiles
Google Maps Scraper With Lead & Contact Enrichment turns a list of cities and search terms into outreach-ready business rows. Each row carries the Google Maps place record — name, address parts, rating, review count, category tags, coordinates, opening hours, place_id — plus a contact block harvested from the business's own website: emails, tel: phone numbers, and profile URLs for seven social platforms. Every row is typed, normalized JSON with the same 29 keys, written into the dataset live. No HTML, no selectors, no parsing. Filter to businesses that have a website — or only to the ones that do not.
📇 Where each contact field actually comes from
This is the part most Google Maps lead scrapers gloss over, so it goes first. There are two completely different data sources feeding one row, and knowing which is which decides whether this Actor fits your workflow.
| Output field | Source | Exact extraction rule |
|---|---|---|
name, tags, notes, avg_rating, total_reviews, street_address, city, state, zip, country_code, full_address, lat, long, place_id, hours | Google Maps listing | Read straight out of the Maps search payload for that place. No second request. |
phone | Google Maps listing | The number Google publishes on the listing. Present whether or not the business has a website. |
website | Google Maps listing | The site Google has on file, with a /url fragment removed and everything after the first ? dropped — a bare origin/path, not a tracking link. |
has_website | Derived | true when website is a non-empty string. This is the flag websiteFilter acts on. |
emails | Business website crawl | Every address matching [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,24} in the page text, in href / content / data-href attributes and in the raw HTML, plus every mailto: target. Then filtered (rules below), lowercased, deduplicated, sorted. |
site_phones | Business website crawl | Only from tel: links — tel:([+0-9().\-\s]{5,30}), whitespace removed. The high-confidence phone list. |
phones_uncertain | Business website crawl | A loose phone-shaped regex over the visible page text, minus anything already in site_phones. Deliberately separate because it is noisy. |
linkedins, facebooks, instagrams, twitters, youtubes, tiktoks, pinterests | Business website crawl | Per-platform URL regexes over text, attributes and raw HTML. Each match normalised to an absolute https:// URL, trailing punctuation trimmed, deduplicated, sorted. |
success | Constant | Always true. The Actor never writes a failure row, so this is a schema-compatibility field, not a quality signal. |
What the website crawl actually does
When scrapeContacts is on and a place has a website, the Actor runs a shallow crawl of that site — at most 5 pages:
- Fetch and parse the homepage. The URL gets an
https://prefix if the Maps listing stored it without a scheme. - Read every
<a href>on that page, skippingmailto:,tel:,javascript:anddata:. Resolve the rest to absolute URLs, drop anything on a different host, and deduplicate ignoring query string and fragment. - Score each surviving link by how many of these hints appear in the lowercased URL: contact, kontakt, about, impressum, imprint, team, connect, reach, support, info, legal, privacy. Links scoring zero are discarded — the crawl never wanders into a blog or a product catalogue.
- Fetch the top 4 by score, parse each, and merge everything into one deduplicated, sorted set per field.
Each page gets a 20-second timeout and goes through the escalating proxy ladder described below. If the homepage cannot be fetched at all, the row is still saved — with every contact array empty.
How much of your list gets a contact
It depends entirely on whether the business publishes it. No hit-rate is quoted anywhere in this README, because the only honest answer is a chain of conditions, and every one of them is a property of the target site, not of the scraper:
- The place must have a website on its Maps listing. Rows where
has_websiteisfalseare never enriched — there is nothing to crawl. - The site must be reachable. A homepage that blocks every rung of the proxy ladder yields empty arrays, indistinguishable in the output from a site that simply has no contact details.
- The address, phone or social link must exist in the HTML of the homepage or one of the four contact-ish pages. Emails injected by JavaScript after load, rendered as an image, hidden behind a contact form, or scrambled with Cloudflare email obfuscation are not recovered — the Actor reads the HTML it is served, it does not run a browser. A site whose only contact route is a form returns no
emailsat all, which is common and completely normal.
By design the Actor never guesses an address from a pattern like firstname@domain.com, never validates deliverability, and never invents a value to fill a column. A missing contact comes back as an empty array, so emails.length > 0 is a truthful filter.
The junk filter on emails
Scraping raw HTML for @ signs produces garbage, so emails is filtered before it reaches you. An address is rejected when:
- It ends in an asset extension —
.png,.jpg,.jpeg,.gif,.webp,.svg,.bmp,.tif,.tiff,.css,.js,.woff,.woff2. - Its domain ends with one of 21 known CDN, site-builder, error-tracking or placeholder hosts — among them
wixpress.com,parastorage.com,sentry.io,googleapis.com,gstatic.com,cloudfront.net,jsdelivr.net,unpkg.com,w3.org,schema.org,example.com,yourdomain.com,wix.com,godaddy.comandsquarespace.com. - Its local part is a retina asset marker (
2x,3x), a hex hash of 16 or more characters (Sentry DSNs), longer than 64 characters, or contains whitespace.
The pattern itself disallows / in the local part and requires an alphabetic TLD, so CDN version strings like react@18.3.1 and coordinate links like @40.70 never become fake leads.
⚠️ Read this before you use phones_uncertain
The two phone columns are different confidence tiers, kept apart on purpose. site_phones comes only from tel: hyperlinks — a business that wrote tel:+12125550100 into its markup meant that to be a phone number, so treat it as reliable. phones_uncertain is a wide net over page text whose pattern matches any unbroken run of 6 to 15 digits, so order numbers, postcodes, prices, VAT numbers and dates land in it. It exists because on some sites it is the only place a number appears at all — but it is a research field, not a dial list.
Social profiles: arrays, not single links
Every platform key is an array, because a homepage footer routinely links a company page and a founder's personal profile. All matches are kept. Each regex also filters out platform noise: Twitter/X matches are dropped when preceded by business., help., about., blog., careers., developer. or ads., and reserved paths such as login, intent, share, hashtag and compose/post are excluded; Instagram skips explore, _n and _u; Facebook skips its own platform paths and accepts profile.php?id=…, pages/…, or a 5–51 character vanity name. YouTube matches youtu.be, /watch, /embed, /user, /c, /channel and /@handle. TikTok matches /@handle, /video/ and /trending. Pinterest matches /pin/<id> and board or user paths.
What is Google Maps Scraper With Lead & Contact Enrichment?
Google Maps Scraper With Lead & Contact Enrichment is an Apify Actor that searches Google Maps by location and keyword — or from Google Maps search URLs you paste in — and returns one flat dataset row per unique business. On top of the standard place record it optionally crawls each business's own website and attaches emails, phone numbers and social profile URLs to the same row, so discovery and enrichment happen in a single run rather than two.
No Google account, no Google Cloud project, and no API key are involved. The Actor sends anonymous requests to the public Google Maps search surface and to public business websites. The only credential in play is your Apify token, and only if you start the run through the API instead of the Console.
- Scrape Google Maps places — name, address parts, full address, rating, review count, category tags, listing phone, coordinates,
place_id - Scrape opening hours as structured per-day records, not a single blob of text
- Enrich each business with website contacts —
emails,site_phonesfromtel:links, and a separate low-confidencephones_uncertainlist - Enrich each business with social profiles across seven platforms, with an optional platform allow-list
- Filter by website presence before rows are saved — including a
withoutWebsiteprospect mode - Export as JSON, CSV or Excel. No proxy management, no parsing
What data does Google Maps Scraper With Lead & Contact Enrichment collect?
Five field groups arrive on one flat row: the Google Maps place, its address and coordinates, its structured opening hours, the contact block crawled from the business website, and the social profile block from that same crawl.
| Data Type | Key Fields | JSON Field Names |
|---|---|---|
| Google Maps place | Business name, category tags, Google's short listing note, rating and review volume, listing phone, website, Google's place identifier | name, tags, notes, avg_rating, total_reviews, phone, website, has_website, place_id, success |
| Location and address | Street, city, state, postcode, country code, a joined address string, and map coordinates | street_address, city, state, zip, country_code, full_address, lat, long |
| Opening hours | One record per day block, with the human-readable range and the 24-hour open/close values Google encodes alongside it | hours → day, hours, open24Hour, close24Hour |
| Website contacts | Email addresses, tel:-link phone numbers, and loosely matched phone-shaped strings kept separately | emails, site_phones, phones_uncertain |
| Social profiles | Profile and page URLs found on the business website, one array per platform | linkedins, facebooks, instagrams, twitters, youtubes, tiktoks, pinterests |
Two naming traps.
phoneandsite_phonesare not the same thing:phoneis what Google Maps publishes on the listing,site_phonesis what the business's own website links withtel:. They frequently differ — a chain's Maps listing may carry a branch line while the site links head office. Keep both. Andhoursis an array whose entries themselves contain a key calledhours(the human-readable range), so a naive flatten collides; address the inner value ashours[i].hours.
Need more lead data for the same outreach list?
Google Maps covers businesses with a physical presence. If you are assembling a contact database across channels, LinkedIn Phone Email Finder works the professional side of the same companies, Facebook Search Scraper WhatsApp Contact Leads picks up messaging-first markets, and Instagram Local Phone Leads Scraper covers storefronts that run on social instead of a site. All of them return contact fields in the same style, so one normalisation step covers the set.
How does this Actor differ from the official Google Places API?
Google's Places API is a paid, key-authenticated service: you create a Google Cloud project, enable billing, and request documented fields through documented endpoints. This Actor requires none of that — it reads the same public Maps search surface a browser does, and then goes somewhere the Places API deliberately does not: the business's own website.
| Feature | Google Places API | This Actor |
|---|---|---|
| Access | Google Cloud project, API key, billing account enabled | ✅ Apify account — open the Actor, press Start |
| Search input | The request parameters Google documents for its text and nearby search endpoints | Arrays of locations × keywords, plus Google Maps /search/ URLs, in one run |
| Business email addresses | Not among the fields Google's Places API documents | ✅ emails, crawled from the business's own website |
| Social profile links | Not among the fields Google's Places API documents | ✅ Seven per-platform arrays, with an allow-list |
| Businesses without a website | The response carries a website field when Google has one, but there is no server-side "has no website" filter in the request | ✅ websiteFilter: "withoutWebsite" — applied before a row is ever saved or charged |
| Quotas, pricing and caching terms | Defined in Google Maps Platform's own documentation and terms — read them for your project | Rows land in your own Apify dataset; you export and keep them |
| Output shape | Google's documented response schema, per requested field mask | One flat row with the same 29 keys every time |
Use the Places API when you need Google's own guarantees, an officially supported contract, and data you intend to display inside a Google-Maps-based product. Use this Actor when what you actually need is a prospect list — including the email addresses and social profiles Google never returns, and the no-website segment Google gives you no way to ask for.
Why do developers and teams scrape Google Maps for leads?
A Google Maps search result set is a local business directory with reachability attached. Four groups get very different value out of turning it into rows.
For lead generation and sales teams
Set locations to your territory, keywords to the verticals you sell into and websiteFilter to withWebsite, and every saved row arrives with emails, site_phones and the social arrays already attached — no second enrichment tool, no second bill. Because phone (from Maps) and site_phones (from tel: links) are separate columns, a dialler list and an email list fall out of the same run without deduplication guesswork. avg_rating and total_reviews are your prioritisation proxy; tags gives you the category to personalise the opening line.
For web design, SEO and local marketing agencies
websiteFilter: "withoutWebsite" is the reason this Actor exists in its current shape. Set it and the dataset contains only businesses whose Google Maps listing has no website on file — a qualified prospect list for anyone selling websites, local SEO or Google Business Profile management. Because the filter runs before the row is written, those runs are cheaper and the export needs no cleanup. The trade-off is inherent: no website means no email enrichment, so you work from phone, full_address, avg_rating and total_reviews and reach them by phone or in person.
For market researchers and location analysts
Everything returned is public business information — trading name, address, category, rating, review count, coordinates — usable for local market studies without touching gated or personal content. Sweep the same keywords across a list of locations and you get comparable business-density and rating distributions per city, keyed on a stable place_id. Re-run on an Apify schedule and diff avg_rating and total_reviews per place_id to track how a market moves. has_website alone is a useful digital-maturity indicator across a category.
For AI engineers and developers building data products
Every row is typed JSON with fixed keys, so nothing sits between the Actor and a model. Index name, tags, notes, full_address and hours into a vector store and an agent can answer "which dentists near this address are open on Saturday" against live data. For a product pipeline, has_website and emails.length are clean boolean gates: route enriched rows to a CRM sync, no-website rows to a phone queue. Call the Actor over the Apify API from any language, or trigger downstream work with an Apify webhook the moment a run finishes.
How to scrape Google Maps leads (step by step)
This Actor runs on Apify. Start it from the Apify Console, or call it through the Apify API with your Apify token. There is no separate signup, no API key of its own, and no other run surface.
- Open Google Maps Scraper With Lead & Contact Enrichment on the Apify Store and click Try for free.
- Fill in 🔎 What to find (keywords) (
keywords) — this is the only required input. Add one entry per vertical:dentists,plumbers,coworking. - Fill in 📍 Cities, regions, or neighborhoods (
locations), or paste Google Maps search links into 🔗 Optional: Google Maps links (urls). You need at least one of the two — with neither, the run ends immediately. - Set 🎯 How many places to collect (
maxResults), pick a 🌐 Website-presence filter (websiteFilter), and leave 📧 Enrich with website contacts (scrapeContacts) on unless you only want the Maps fields. - Click Start. Rows appear in the output table live as they are found. Export the dataset as JSON, CSV or Excel, or read it through the Apify API.
Every location is combined with every keyword, so 3 locations × 4 keywords is 12 searches in one run, all deduplicated against a single shared place_id set and all counting toward one maxResults budget.
What to do when Google changes its structure
Nothing on your side. The Actor reads Google Maps' internal search payload rather than rendered page markup, so a visual redesign of the Maps interface generally does not touch it, and each field is read through a bounded index walk that returns null instead of raising when Google nests a value differently. The scraper is maintained and the output keys stay the same, so your integrations keep working.
⬇️ Input
Eight parameters, read directly from the Actor's input schema. Only keywords is required by the schema — but in practice you also need at least one locations entry or one urls entry, or the run has nothing to search.
| Parameter | Required | Type | Description | Example Value |
|---|---|---|---|---|
keywords | ✅ Yes | array of strings | What to find — the business types or place descriptions you care about. Every keyword is combined with every location, so adding keywords multiplies your searches. Blank and whitespace-only entries are dropped. Editor is a string list; the Console form prefills ["coffee shops"]. No default value. | ["dentists", "orthodontists"] |
locations | No | array of strings | Cities, states, districts, neighbourhoods — anything you would type into a map app. Each is geocoded to a viewport before searching. Blank entries are dropped. Editor is a string list; the Console form prefills ["New York"]. No default value. Optional only if you supply urls. | ["Austin, Texas", "Round Rock, Texas"] |
urls | No | array | Google Maps search links, if you already have the exact view you want. Accepts plain strings or {"url": "…"} objects. Editor is a string list; prefilled empty. No default value. Processed after the locations × keywords sweep, and only if maxResults has not already been reached. | ["https://www.google.com/maps/search/coffee+shops/@30.2672,-97.7431,13z"] |
maxResults | No | integer | Total unique places saved across all locations, keywords and URLs combined — duplicates are merged, not counted twice. Minimum 1, maximum 10000, default 20. | 200 |
scrapeContacts | No | boolean | On: shallow-crawl each business website (homepage plus up to 4 contact/about/impressum pages) for emails, tel: phones and social links. Off: Google Maps fields only, and the contact arrays come back empty. Default true. | true |
websiteFilter | No | string (select) | Website-presence filter, applied before a row is saved. allPlaces — no filter. withWebsite — only businesses that have a site, so every row is enrichable. withoutWebsite — only businesses with no site on their listing. Default "allPlaces". Any value outside these three is silently treated as allPlaces. | "withoutWebsite" |
scrapeSocialMediaProfiles | No | object (JSON) | Allow-list of which social platforms to emit. Set the platforms you want to true. Recognised keys: linkedins, facebooks, instagrams, twitters, youtubes, tiktoks, pinterests — the singular forms linkedin, facebook, instagram, twitter, x, youtube, tiktok, pinterest are accepted as aliases. Default {} — an empty object emits every platform found. Non-allowed platforms are returned as empty arrays, never removed as keys. | {"linkedins": true, "instagrams": true} |
proxyConfiguration | No | object (proxy editor) | Apify Proxy settings. Prefilled {"useApifyProxy": false} — the Actor starts on a direct connection and escalates automatically if Google pushes back. Turn it on for heavier runs or restrictive networks. When on, the Actor starts on residential if RESIDENTIAL is listed in apifyProxyGroups, otherwise on datacenter. | {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]} |
Behaviour worth knowing before you configure it
websiteFilternever errors. A typo like"withwebsite"is silently coerced toallPlacesand the run proceeds unfiltered. Check the value, the run log states the filter it settled on.- Setting every platform to
falseis the same as leaving the object empty. The allow-list is built from keys whose value is truthy; if that set comes out empty, the Actor falls back to emitting all platforms. To suppress socials entirely, turnscrapeContactsoff — there is no "no socials" setting. scrapeContacts: truewithwebsiteFilter: "withoutWebsite"enriches nothing. The two are mutually exclusive by definition: rows in that mode have no website to crawl, so the website crawler is not even initialised for the run. Contact arrays come back empty.urlsonly run if budget is left. The locations × keywords sweep goes first. If it fillsmaxResults, your URLs are never opened. Put URLs in a run of their own if they matter.- Use
/search/Maps URLs. A link that only carries@lat,lngcoordinates has no query for the Actor to extract, and the raw URL string ends up being used as the search term — which finds nothing. A URL of the formhttps://www.google.com/maps/search/<query>/@lat,lng,zoomgives the Actor both the query and the viewport. - Geocoding takes the first match. Locations are resolved through OpenStreetMap's Nominatim service with
limit=1. Ambiguous names resolve to whichever entry ranks first, so writeSpringfield, Illinoisrather thanSpringfield. The run log prints the resolved centre coordinates and country — check them on the first run of a new territory.
Example input
{"locations": ["Austin, Texas", "Round Rock, Texas"],"keywords": ["dentists", "orthodontists"],"urls": [],"maxResults": 200,"scrapeContacts": true,"websiteFilter": "withWebsite","scrapeSocialMediaProfiles": {"linkedins": true,"facebooks": true,"instagrams": true},"proxyConfiguration": {"useApifyProxy": true,"apifyProxyGroups": ["RESIDENTIAL"]}}
No-website prospect list
{"locations": ["Manchester, UK"],"keywords": ["plumbers", "electricians", "roofers"],"maxResults": 500,"scrapeContacts": false,"websiteFilter": "withoutWebsite","proxyConfiguration": { "useApifyProxy": true }}
Common pitfall: leaving maxResults at its default of 20 while supplying several locations and keywords. The budget is a total across the whole run, not a per-search figure — 3 locations × 4 keywords with maxResults: 20 stops after 20 rows, most likely inside the very first search. Size the budget to the number of combinations you asked for.
⬆️ Output
Every place is one flat dataset row with the same 29 keys, run after run. Contact and social fields are always present as arrays — empty when nothing was found, never missing — so downstream code never needs a presence check. Google Maps fields return null when Google does not publish the value. Export as JSON, CSV or Excel, or read the dataset through the Apify API.
The dataset contains only place rows. The Actor pushes no summary, accounting or error records, so there is no filter expression you need to strip anything out — every item you see is a business. Run-level information (areas resolved, tiles completed, enrichment counts, failures) goes to the run log. A merged copy of everything collected is also written to the run's default Key-Value Store as maps.json.
Useful filters on the export: has_website == true for enrichable businesses, emails.length > 0 for rows that actually produced an address, site_phones.length > 0 for high-confidence dial targets.
Scraped place with website enrichment
{"street_address": "1201 W 6th St","city": "Austin","zip": "78703","state": "TX","country_code": "US","full_address": "1201 W 6th St Austin TX 78703 US","website": "https://westsixthdental.com","avg_rating": 4.8,"total_reviews": 412,"name": "West Sixth Dental","tags": ["Dentist", "Cosmetic dentist", "Dental clinic"],"notes": "Modern dental practice offering same-day crowns.","place_id": "0x8644b50e1f0a2c9d:0x7f3b21c4a95e8d10","phone": "+1 512-555-0142","lat": 30.271129,"long": -97.756418,"hours": [{ "day": "Monday", "hours": "8 AM-5 PM", "open24Hour": "0800", "close24Hour": "1700" },{ "day": "Tuesday", "hours": "8 AM-5 PM", "open24Hour": "0800", "close24Hour": "1700" },{ "day": "Friday", "hours": "8 AM-2 PM", "open24Hour": "0800", "close24Hour": "1400" },{ "day": "Saturday", "hours": "Closed", "open24Hour": null, "close24Hour": null }],"has_website": true,"emails": ["hello@westsixthdental.com", "scheduling@westsixthdental.com"],"site_phones": ["+15125550142", "+15125550188"],"phones_uncertain": ["512 555 0142", "78703"],"linkedins": ["https://linkedin.com/company/west-sixth-dental"],"facebooks": ["https://facebook.com/westsixthdental"],"instagrams": ["https://instagram.com/westsixthdental"],"twitters": [],"youtubes": ["https://youtube.com/@westsixthdental"],"tiktoks": [],"pinterests": [],"success": true}
Note phones_uncertain in that example: 78703 is the postcode, matched because it is a run of digits in the page text. That is exactly the noise the separate column exists to isolate.
Scraped place with no website — the withoutWebsite prospect row
Same 29 keys, same types. The Maps half of the row is unchanged — name, address parts, phone, avg_rating, total_reviews, place_id, coordinates and hours are all populated exactly as above. Only the website half differs, and this is the row websiteFilter: "withoutWebsite" keeps and withWebsite drops:
{"name": "South Austin Plumbing Co","phone": "+1 512-555-0119","full_address": "4402 Menchaca Rd Austin TX 78745 US","website": "","has_website": false,"emails": [],"site_phones": [],"phones_uncertain": [],"linkedins": [],"facebooks": [],"instagrams": [],"twitters": [],"youtubes": [],"tiktoks": [],"pinterests": [],"success": true}
Field notes that will save you a debugging session
full_addressis space-joined, not comma-joined. It is built asstreet city state zip country_codewith only the outer whitespace trimmed, so a missing middle component leaves a double space inside the string. For clean formatting, compose it yourself from the five parts — all of them are on the row.websitehas its query string removed. Everything after the first?is dropped, which kills tracking parameters but also loses a listing URL that genuinely depends on a query string.avg_ratingandtotal_reviewsare bothnullwhen Google publishes no rating. New listings frequently have neither.hourscan be an empty array when Google publishes no opening hours. Individual entries can carry"hours": nullwhen a day block has no readable range;open24Hourandclose24Hourare Google's own encoded values mirroring the readable string.place_idis Google's0x…:0x…hexadecimal identifier and the deduplication key for the run. It is stable across runs, so it is the right join key for change tracking.tagsandnotescan benull.tagsis Google's category list;notesis the short descriptive line Google sometimes attaches, absent far more often than present.
How does Google Maps Scraper With Lead & Contact Enrichment compare to other Google Maps scrapers?
Google Maps is the most crowded category on the Apify Store, so the useful comparison is not "does it scrape Maps" — they all do. It is what happens to the row after Google hands it over.
| Feature | This Actor | Generic alternative |
|---|---|---|
| Businesses without a website | ✅ websiteFilter: "withoutWebsite" produces a no-website prospect list, filtered before rows are saved | Returns everything; you filter for empty website columns in a spreadsheet afterwards, and pay for the rows you throw away |
| Search input shape | ✅ Arrays of locations × keywords combined into one deduplicated run, plus Maps URLs | Commonly a single search-query string, or one location string per run |
| Phone confidence | ✅ Three separate columns — phone (Maps), site_phones (tel: links only), phones_uncertain (loose text match) | A single merged phone field, with regex noise mixed into real numbers |
| Social profile output | ✅ Seven per-platform arrays, all matches kept, plus a platform allow-list | Commonly one URL per platform, extra profiles discarded |
| Row schema | ✅ Same 29 keys on every row; contacts are empty arrays, never absent keys | Fields commonly omitted when empty, so consumers need presence checks |
Three specific listings, all checked on the Apify Store on 2026-07-25 — none of this is measured here, it is what their own listings state:
george.the.developer/google-maps-lead-intel— its listing advertises email deliverability validation and a 0–100 lead score, from a documented input of a singlesearchQuerystring plusmaxResults(documented range 1–100) andminRating. Its documented output nests socials as onesocialProfilesobject with a single URL per platform.scraplib/google-maps-lead-generation— its listing documents three inputs (searchTerms, onelocationstring,maxResults) and an output with onesocialLinksobject holding a single optional URL per platform.scrapemint/google-maps-scraper— its listing documents review text and reviewer details per place, plus optional website enrichment producing emails and person names with job titles, and states "First 2 places per run are free."
What this Actor deliberately does not do: it does not validate email deliverability, compute a lead score, or scrape review text and reviewer profiles. If deliverability validation is the feature you are buying, google-maps-lead-intel advertises it and this Actor does not; if review content is the feature, scrapemint/google-maps-scraper advertises it and this Actor does not. What this one does that none of the three listings documents is produce a no-website prospect segment, filtered before rows are written, from a multi-location multi-keyword sweep, with phone confidence kept in separate columns.
If you are building an AI agent or a RAG pipeline, the row-schema line is the decision-maker — parsing HTML inside an agent loop is a reliability failure mode, not a feature. If you are building an outreach list, the first line is: filtering before rows are written keeps the dataset clean and the run cheap.
How many results can you scrape with Google Maps Scraper With Lead & Contact Enrichment?
maxResults caps the number of unique places saved to the dataset, with a minimum of 1, a maximum of 10000, and a default of 20. It is a total across every location, keyword and URL in the run — not a per-search figure — and duplicates are merged against a shared place_id set rather than counted twice.
There is no page-by-page pagination. Coverage comes from a geographic grid instead. Each location is geocoded to a bounding box, and the Actor lays a square grid of search points over that box, issuing one Google Maps search per point. Grid density scales with how many results are still outstanding:
| Results still needed | Grid | Search points per keyword |
|---|---|---|
| Up to 50 | 6 × 6 | 36 |
| 51 – 100 | 8 × 8 | 64 |
| 101 – 200 | 10 × 10 | 100 |
| Above 200 | 12 × 12 | 144 |
Every point requests up to 120 places from Google, all points for a keyword are dispatched concurrently, and results stream into the dataset as tiles complete. Once the budget is met the remaining tile requests are cancelled immediately rather than being paid for and discarded. Grid density is chosen from the remaining need, so later keyword/location pairs in a large run use progressively smaller grids as the budget fills.
Two practical ceilings follow. A very large maxResults over a small area exhausts what Google returns for that viewport before it exhausts your budget — the run finishes short, which is correct behaviour, not a failure. And a wide area such as a whole state resolves to a large bounding box that a 6 × 6 grid samples coarsely; for dense metro coverage, list neighbourhoods as separate locations entries rather than relying on one broad name.
How does the Actor handle Google's anti-bot defences and website blocking?
Two different targets, two independent escalation ladders. Neither requires any configuration from you.
Google Maps requests. Each request carries a desktop Chrome user agent and a Google referer; the connection pool is capped at 12 concurrent sockets, with a 30-second total timeout and up to 3 attempts spaced one second apart. A response counts as blocked on status 429, 403 or 503, or when the body matches unusual traffic, sorry.*automated, verify.*you are not a robot, detected.*traffic, captcha or rate limit. On a block the Actor climbs one rung — direct → Apify datacenter proxy → Apify residential proxy — and once it reaches residential it stays there for the rest of the run. Enabling Apify Proxy in the input simply starts the run higher up: residential when RESIDENTIAL is in your apifyProxyGroups, datacenter otherwise.
Business website crawls. These use a separate HTTP client with browser-accurate TLS and HTTP/2 impersonation (Chrome fingerprint, HTTP/3 enabled), so an ordinary business site behind a WAF sees a normal browser handshake instead of a Python client. The same three rungs apply with their own state: blocked means status 401, 403, 407, 429 or any 5xx, or /sorry/, unusual traffic or captcha in the first 2000 characters. Residential is retried up to 3 times with exponential backoff plus jitter before the fetch is abandoned, and proxied rungs rotate the exit IP on every request. The ratchet only goes up, never back down.
When a site defeats all three rungs the row is still saved with empty contact arrays and the run continues — one business's WAF never fails your run.
💳 Pricing and what gets charged
This Actor uses Apify's pay-per-event pricing with a single event: row_result, charged once for each place row written to the dataset.
You are not charged for:
- Places filtered out by
websiteFilter— the filter runs before the push, so awithoutWebsiterun never pays for the businesses that have a site. - Duplicate places already seen in the run — deduplication on
place_idalso happens before the push. - Grid tiles that return nothing, Google requests that get blocked and retried, and geocoding lookups including ones that fail to resolve.
- Website crawls, whether they succeed, come back empty, or get blocked — enrichment rides along with its row, there is no separate enrichment event.
- The
maps.jsoncopy written to the Key-Value Store.
Because the dataset contains nothing but place rows, there is no accounting or error record to exclude and no filter expression to apply before counting your leads. One dataset item equals one charged row_result.
The nuance worth planning around: the charge is for the row, not for the enrichment work behind it. A row that produced no email costs the same as one that produced three. If you only want rows that converted into a contact, filter the export on emails.length > 0 — but the row was charged when it was saved. Runs also consume standard Apify platform usage (compute units, plus proxy traffic if you enable Apify Proxy), billed by Apify in the usual way.
Is it legal to scrape Google Maps?
Scraping publicly listed business information is broadly treated as permissible where the data is accessible without authentication, and this Actor collects only what an anonymous visitor already sees: public Google Maps listings and the public pages of business websites. No login, no cookie, no account.
Most of what comes back is business record data — trading name, street address, category, opening hours, rating, review count, coordinates. That is commercial information about an entity, not personal data, and the framework that applies to it is contractual and database-related: Google's terms of service, and local database or unfair-competition rules covering systematic reuse of a compiled directory.
The contact block needs a second look. A generic mailbox such as info@ or hello@ is a business contact point. An address of the form firstname.lastname@ identifies a person, and in that case GDPR, CCPA and equivalent regimes do apply to your storage and use of that row — you need a lawful basis, and outreach to it falls under the marketing rules of the recipient's jurisdiction. The same applies to a personal LinkedIn or Instagram profile that appears in a social array alongside the company page.
Consult legal counsel for commercial use cases involving bulk personal data.
❓ Frequently asked questions
Does this Actor work without a Google account or a Google API key?
Yes. No Google account, no Google Cloud project, no Places API key, no cookie and no session are used. The Actor sends anonymous requests to Google's public Maps search surface and to public business websites. The only credential involved is your Apify token, and only if you start the run through the Apify API rather than clicking Start in the Console.
How often is the scraped data updated?
Every run fetches live. Nothing is cached between runs — Maps searches, geocoding lookups and website crawls are all requested fresh, so a row reflects what Google and the business site published at the moment your run touched them. The only reuse within a run is the place_id deduplication set. Use Apify's scheduler for a recurring refresh, and diff on place_id to track what changed.
What happens when a business has no website, or its website cannot be crawled?
The row is still saved and still complete. If has_website is false there is nothing to crawl, so emails, site_phones, phones_uncertain and all seven social arrays come back empty — which is the entire point of the withoutWebsite prospect mode. If the business has a site but the homepage cannot be fetched through any rung of the proxy ladder, the output is identical: empty arrays, has_website: true, and a warning in the run log naming the site. Nothing partial or invented is written either way.
Can I get emails that a business does not publish on its website?
No, and the Actor does not pretend otherwise. It extracts only addresses physically present in the HTML of the homepage or one of up to four contact/about/impressum pages. It does not guess patterns like firstname@domain.com, query a third-party enrichment database, run JavaScript to reveal lazily-injected addresses, decode Cloudflare email obfuscation, or read text out of images. A business whose only contact route is a web form returns an empty emails array — accurately.
Am I charged for places that get filtered out or deduplicated?
No. The row_result event is charged on the push, and both the websiteFilter check and place_id deduplication run before it. A withoutWebsite run does not pay for businesses that have a site, and a business found by three different keywords is charged once. Blocked requests, failed geocodes, empty grid tiles and website crawls are never charged.
Does this Actor work for AI agent workflows and LLM pipelines?
Yes. It is a standard HTTP-triggered Apify Actor, so LangChain, CrewAI, n8n, or a hand-written tool definition can start a run through the Apify API and read typed JSON back with no parsing step. Every row has the same 29 keys, so a tool schema written once stays valid. has_website and the length of emails are clean boolean gates for agent routing, and name, tags, notes, full_address and hours index straight into a vector store.
How does the Actor handle Google's anti-bot system?
With an escalating proxy ladder — direct, then Apify datacenter, then Apify residential, sticky once reached — driven by block detection on both HTTP status (429, 403, 503) and body content (unusual traffic, automated-query notices, robot verification prompts, captcha, rate limit). Requests carry a desktop Chrome user agent and a Google referer, concurrency is capped at 12 sockets, and each URL gets up to 3 attempts. The website crawler runs its own ladder with Chrome TLS and HTTP/2 impersonation.
How does it compare to other Google Maps scrapers on the Store?
Checked on the Apify Store on 2026-07-25, taken from their own listings rather than measured here: george.the.developer/google-maps-lead-intel advertises email validation and a 0–100 lead score from a single searchQuery input; scraplib/google-maps-lead-generation documents searchTerms plus one location string and one social URL per platform; scrapemint/google-maps-scraper documents review text and reviewer details plus optional website enrichment. This Actor's differences are structural: a withoutWebsite prospect filter applied before rows are saved, multi-location × multi-keyword input in one deduplicated run, three phone confidence columns, and socials as arrays with a platform allow-list. It has no lead score and no email validation.
Can I use it without managing proxies?
Yes. Leave proxyConfiguration at its default and the Actor starts on a direct connection, detects blocking on both ladders, and escalates to Apify datacenter and then Apify residential proxies on its own — creating sessions, rotating exit IPs on proxied rungs, and holding residential once it gets there. Enabling Apify Proxy in the input simply changes the starting rung: residential when RESIDENTIAL is in apifyProxyGroups, datacenter otherwise.
Does it return data in a format LLMs can use directly?
Yes. Typed, normalized JSON with stable field names on every run — no HTML, no selectors, no parsing. Contact and social fields are always arrays, empty rather than absent, so a model or a downstream schema never encounters a missing key. Pass a row straight into an LLM context window, index it into a vector store, or hand it to an agent tool.
What happens when Google changes its structure or blocks the scraper?
The scraper is maintained, and because it reads Google Maps' internal search payload rather than rendered markup, a visual redesign of the Maps interface generally does not affect it. Individual fields are read through a bounded index walk that returns null instead of raising when a value moves, so a partial change degrades one field rather than breaking the run. Your field names and types do not change on your end.
Integrate Google Maps Scraper With Lead & Contact Enrichment and automate your workflow
The Actor works with any language or tool that can send an HTTP request to the Apify API.
REST API integration
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("<YOUR_USERNAME>/google-maps-scraper-with-lead-contact-enrichment").call(run_input={"locations": ["Austin, Texas", "Round Rock, Texas"],"keywords": ["dentists", "orthodontists"],"maxResults": 200,"scrapeContacts": True,"websiteFilter": "withWebsite","proxyConfiguration": {"useApifyProxy": True, "apifyProxyGroups": ["RESIDENTIAL"]},})for place in client.dataset(run["defaultDatasetId"]).iterate_items():email = place["emails"][0] if place["emails"] else Noneprint(place["name"], place["full_address"], place["phone"], email)
Works in Python, Node.js, Go, Ruby, cURL.
Automation platforms (n8n, Make, LangChain)
In n8n, use the Apify node — or an HTTP Request node pointed at the Apify run endpoint with your token — and pass the same JSON input shown above; an IF node on has_website splits enrichable leads from no-website prospects, and a Set node flattens emails[0] into a single column for a mail-merge step. In Make, the Apify module supports run-and-wait, so a weekly territory sweep feeds a Google Sheets, Airtable, HubSpot or Pipedrive module directly through an Iterator over the dataset items. In LangChain, wrap the Apify run call as a tool and hand the rows to the model unchanged — they are already typed JSON with fixed keys. Apify schedules handle recurring runs, and Apify webhooks fire your own endpoint when a run finishes, so a CRM sync starts without polling.
Related scrapers
| Scraper Name | What it extracts |
|---|---|
| LinkedIn Phone Email Finder | Phone and email contact points for LinkedIn profiles |
| Facebook Search Scraper WhatsApp Contact Leads | Facebook search results with WhatsApp contact leads |
| Instagram Local Phone Leads Scraper | Local business Instagram profiles with published phone contacts |
| Instagram Profile Bio Contact Extractor | Contact details published in Instagram profile bios |
| Realtor.com Agent Scraper With Phone & Social Leads | Real estate agent records with phone and social contact points |
| Alibaba Scraper With Supplier Contact Leads | Supplier listings with email, phone and website enrichment |
| TikTok Profile Scraper With Bio Link Contact Leads | TikTok profiles with bio links and contact leads |
💬 Your feedback
Found a bug, or missing a field that Google Maps publishes but the output does not carry? We want to know — open an issue on the Actor's Issues tab and it will be looked at. Reports that include the exact input JSON you ran, and the business name or place_id of a row that came out wrong, are the fastest to reproduce and fix. Requests for additional contact sources or extra social platforms are welcome too.
