Google Maps Leads Scraper
Pricing
Pay per event
Google Maps Leads Scraper
Extract qualified B2B leads from Google Maps by search query and location. Returns business name, address, phone, website, rating, review count, category, coordinates, and emails harvested from the linked website. Built for outbound sales and territory research.
Pricing
Pay per event
Rating
0.0
(0)
Developer
Hasnain Nisar
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
7 days ago
Last modified
Categories
Share
Google Maps Scraper — Apify Actor
A drop-in Apify actor that turns either a natural-language prompt ("find 50 dentists in Dubai with email") or a list of search queries into a clean dataset of business listings: name, address, phone, website, rating, review count, category, lat/lng — plus, with deep scan enabled, emails, phone numbers, social-media links, and named contacts harvested from each business's website.
Built on Playwright + Chromium with Apify Proxy support and Groq-powered prompt parsing.
What it does
For each search term (either typed in directly or extracted from your prompt), the actor:
- Opens
https://www.google.com/maps/search/<term> in <location> - Scrolls the left-hand results panel until either
maxResultscards have loaded or Google's "You've reached the end of the list" sentinel appears - Clicks each card and extracts the structured fields from the detail pane
- (Deep scan) Visits the listing's website + up to two contact-style sub-pages, harvesting emails, phones, social URLs, and named contacts
- Applies your filters (
minRating,hasEmail,noWebsite, etc.) and pushes each kept record to the dataset as soon as it's produced — partial runs stay useful
Two ways to drive it
1. Prompt mode (recommended)
Just describe what you want:
{"prompt": "find 50 dentists in Dubai with email and rating above 4","groqApiKey": "<your-groq-key>","deepScan": true}
The actor calls Groq's llama-3.3-70b-versatile to parse that into:
{"query": "Dentists in Dubai, United Arab Emirates","city": "Dubai","country": "United Arab Emirates","category": "Dentists","suggested_max": 50,"deep_scan": true,"filters": {"has_email": true,"min_rating": 4.0},"priority_fields": ["email"]}
Then runs the scrape with those settings. No Groq key? A built-in rule-based parser handles the basics ("50 plumbers in NYC", "dentists in London with phone") without any LLM call — accuracy dips for ambiguous phrasings, but it's free and offline.
You can still override individual fields. If you supply both prompt: "100 dentists" and searchTerms: ["orthodontists"], the explicit searchTerms wins. This lets you use the prompt as a starting template and tweak pieces.
2. Direct mode
Skip the prompt and pass everything explicitly:
{"searchTerms": ["dentists", "orthodontists"],"location": "Austin, TX","maxResults": 30,"deepScan": true,"filters": { "minRating": 4.5 }}
Deep scan — what it pulls
When deepScan is on, every listing with a website is visited. The crawler extracts:
| Field | Source |
|---|---|
deepScanEmails | All mailto: links + email-shaped strings on homepage and /contact-style sub-pages |
deepScanPhones | All tel: links + phone-shaped strings (filtered to avoid date/license-number noise) |
deepScanSocials | Instagram, Facebook, LinkedIn, X/Twitter, TikTok, YouTube, WhatsApp links |
deepScanIndividuals | Names following titles like "CEO:", "Founder:", "Director:", "Owner:" |
Each is a comma-separated string for spreadsheet-friendly output. Empty when the field couldn't be resolved.
Performance: ~3-8 seconds per listing with a website, with an early-exit once both an email and a phone have been found.
Output schema
Each row in the dataset:
{"name": "Joe's Coffee","category": "Coffee shop","address": "123 W 56th St, New York, NY 10019","phone": "+1 212-555-0142","website": "https://joescoffee.example","email": "hello@joescoffee.example","rating": 4.6,"reviewsCount": 812,"hours": "Open ⋅ Closes 8 PM","plusCode": "Q263+CV New York, NY","latitude": 40.762435,"longitude": -73.978942,"googleMapsUrl": "https://www.google.com/maps/place/Joe's+Coffee/data=...","searchTerm": "coffee shops","deepScanEmails": "hello@joescoffee.example, orders@joescoffee.example","deepScanPhones": "+12125550142, +12125550143","deepScanSocials": "https://instagram.com/joescoffee, https://facebook.com/joescoffee","deepScanIndividuals": "John Doe, Jane Smith"}
deepScan* fields are "" unless deep scan is enabled. Any field not present on the listing comes back as an empty string or null.
Input
| Field | Type | Default | Description |
|---|---|---|---|
prompt | string | "" | Natural-language description. When set, parsed values fill in searchTerms/location/maxResults/filters/deepScan for slots you didn't explicitly provide. |
searchTerms | string[] | ["coffee shops"] | Used when prompt is empty (or to override the parser's category). Each term is searched independently. |
location | string | "New York" | City / region / country. Combined with each term as <term> in <location>. Leave empty to use the term verbatim. |
maxResults | integer | 50 | Hard cap per search term. Maps caps result lists around 120; values above are silently clamped. Prompt's "find X" is honored when this is left at the default. |
language | enum | "en" | Two-letter language code → Google's hl parameter. |
deepScan | boolean | false | Visit each listing's website to extract emails, phones, socials, named contacts. Required for hasEmail/hasWhatsapp filters to work reliably. |
extractEmails | boolean | false | Lightweight email-only lookup. Ignored when deepScan is on. |
extractWebsiteFallback | boolean | true | When extractEmails is on, also try <website>/contact if the homepage has no email. |
filters | object | {} | Post-scrape filter dict — see below. |
groqApiKey | string (secret) | "" | Required for AI prompt parsing. Free at https://console.groq.com. Without one, a rule-based fallback is used. |
headless | boolean | true | Disable only for local debugging. |
proxy | proxy | { useApifyProxy: true } | RESIDENTIAL group strongly recommended. |
Filter object
{"noWebsite": false,"hasPhone": false,"hasEmail": false,"hasWhatsapp": false,"minRating": null,"maxRating": null,"minReviews": null}
All null / false filters are inactive. hasEmail, hasPhone, hasWhatsapp will look in the deep-scan results too when those fields are populated.
Usage
From the Apify console
- Open the actor's Run tab.
- Either type a prompt + paste your Groq key, or fill in
searchTerms/location/maxResults. - Toggle Deep scan if you want emails / phones / socials.
- Start — watch records stream into the dataset as they're produced.
From the Apify CLI
apify call <username>/google-maps-scraper --input='{"prompt": "find 100 plumbers in Brooklyn with email rating above 4","groqApiKey": "gsk_...","deepScan": true}'
Programmatically (Apify client SDK)
from apify_client import ApifyClientclient = ApifyClient("<your-apify-token>")run = client.actor("<username>/google-maps-scraper").call(run_input={"prompt": "50 dental clinics in Karachi Pakistan with phone","groqApiKey": "gsk_...","deepScan": True,})for item in client.dataset(run["defaultDatasetId"]).iterate_items():print(item["name"], "—", item["phone"], "—", item.get("deepScanEmails", ""))
Run locally
cd apify-google-maps-actor# 1) Install deps + Playwright browserpip install -r requirements.txtpython -m playwright install chromium# 2) Provide input via Apify's local-storage conventionmkdir -p storage/key_value_stores/defaultcat > storage/key_value_stores/default/INPUT.json <<'EOF'{"prompt": "10 coffee shops in NYC","deepScan": false,"headless": false}EOF# 3) Runpython -m src
Records are written to storage/datasets/default/ as numbered .json files.
Build & deploy to Apify
cd apify-google-maps-actorapify login # one-timeapify push # builds the Dockerfile remotely & deploys
Or via Git: push this folder to a GitHub repo, then point a new actor on Apify at that repo — the platform builds from .actor/Dockerfile.
Project layout
apify-google-maps-actor/├── .actor/│ ├── actor.json # actor metadata + dataset views (Overview + Deep scan)│ ├── input_schema.json # 12 input fields rendered as the run form│ └── Dockerfile # builds on apify/actor-python-playwright:3.12├── src/│ ├── __init__.py│ ├── __main__.py # entrypoint — Apify lifecycle, prompt parsing, term loop│ ├── scraper.py # Playwright-based Maps scrape (extract + enrich + keep)│ ├── prompt_parser.py # natural-language prompt → structured config (Groq + fallback)│ ├── website_crawler.py # deep-scan: emails, phones, socials, named contacts│ └── filters.py # post-scrape filter predicate (rating, reviews, has_phone, ...)├── requirements.txt├── .dockerignore├── .gitignore└── README.md (this file)
Notes & limits
- Google rate-limiting. Without a proxy, expect blocks after a few hundred requests from one IP. Apify Proxy → RESIDENTIAL is the practical default.
- Maps result caps. Google itself stops listing results around the 120-mark. Splitting one big search into several narrower queries (by neighborhood instead of city) gets past that ceiling.
- Deep-scan coverage. ~30-60% of small-business websites expose an email; the rest hide behind contact forms. The actor never invents an email —
deepScanEmailsis just empty. - Groq parsing. The
llama-3.3-70b-versatilemodel handles ambiguous prompts (locale-specific country/city splits, multiple filters per sentence) much better than the rule-based fallback. Get a free key at https://console.groq.com. - Prompt vs explicit input. Explicit fields always win. The prompt fills in only what you left blank — so you can use the prompt as a base template and override pieces.
License
MIT — same as the parent project.