Google Maps Leads Scraper avatar

Google Maps Leads Scraper

Pricing

Pay per event

Go to Apify Store
Google Maps Leads Scraper

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

Hasnain Nisar

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

7 days ago

Last modified

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:

  1. Opens https://www.google.com/maps/search/<term> in <location>
  2. Scrolls the left-hand results panel until either maxResults cards have loaded or Google's "You've reached the end of the list" sentinel appears
  3. Clicks each card and extracts the structured fields from the detail pane
  4. (Deep scan) Visits the listing's website + up to two contact-style sub-pages, harvesting emails, phones, social URLs, and named contacts
  5. 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

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:

FieldSource
deepScanEmailsAll mailto: links + email-shaped strings on homepage and /contact-style sub-pages
deepScanPhonesAll tel: links + phone-shaped strings (filtered to avoid date/license-number noise)
deepScanSocialsInstagram, Facebook, LinkedIn, X/Twitter, TikTok, YouTube, WhatsApp links
deepScanIndividualsNames 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

FieldTypeDefaultDescription
promptstring""Natural-language description. When set, parsed values fill in searchTerms/location/maxResults/filters/deepScan for slots you didn't explicitly provide.
searchTermsstring[]["coffee shops"]Used when prompt is empty (or to override the parser's category). Each term is searched independently.
locationstring"New York"City / region / country. Combined with each term as <term> in <location>. Leave empty to use the term verbatim.
maxResultsinteger50Hard 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.
languageenum"en"Two-letter language code → Google's hl parameter.
deepScanbooleanfalseVisit each listing's website to extract emails, phones, socials, named contacts. Required for hasEmail/hasWhatsapp filters to work reliably.
extractEmailsbooleanfalseLightweight email-only lookup. Ignored when deepScan is on.
extractWebsiteFallbackbooleantrueWhen extractEmails is on, also try <website>/contact if the homepage has no email.
filtersobject{}Post-scrape filter dict — see below.
groqApiKeystring (secret)""Required for AI prompt parsing. Free at https://console.groq.com. Without one, a rule-based fallback is used.
headlessbooleantrueDisable only for local debugging.
proxyproxy{ 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

  1. Open the actor's Run tab.
  2. Either type a prompt + paste your Groq key, or fill in searchTerms / location / maxResults.
  3. Toggle Deep scan if you want emails / phones / socials.
  4. 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 ApifyClient
client = 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 browser
pip install -r requirements.txt
python -m playwright install chromium
# 2) Provide input via Apify's local-storage convention
mkdir -p storage/key_value_stores/default
cat > storage/key_value_stores/default/INPUT.json <<'EOF'
{
"prompt": "10 coffee shops in NYC",
"deepScan": false,
"headless": false
}
EOF
# 3) Run
python -m src

Records are written to storage/datasets/default/ as numbered .json files.

Build & deploy to Apify

cd apify-google-maps-actor
apify login # one-time
apify 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 — deepScanEmails is just empty.
  • Groq parsing. The llama-3.3-70b-versatile model 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.