Extract local business leads from Google Maps in Python — names, addresses, phones, emails, websites, ratings, reviews, photos, social handles. Geo-drilling unlocks past Google's 120-place cap per query. Built for B2B local-business outreach, market mapping, and review-monitoring workflows.
Working Python project that drives the Google Maps Scraper Apify actor — a focused local-leads extractor optimized for B2B prospecting workflows. Pulls business profile data, reviews, photos, and (its standout feature) emails + social-media handles harvested from each place's own website.
The official Google Places API exists but it's expensive ($17 per 1,000 detailed lookups after free tier), rate-limited, and (critically) does not return business email addresses, social handles, or any deep contact data. It also caps results at 60 places per query no matter what you do. For lead-gen workflows you typically need the email, not just the phone.
This actor scrapes Google Maps' public results, follows each business's website link, runs a contact-extraction pass on the homepage + contact / about pages, and returns the email + social handles inline with each place record. It also breaks through Google's 120-place query cap via geo-drilling: when a search area saturates (≥20 results), the actor automatically subdivides the area into sub-areas and re-queries. This is how you go from "the top 60 plumbers in Chicago" to "every plumber in Chicago".
- Local B2B outreach lead lists — pull every restaurant / dentist / lawyer / plumber in a city with email + phone for SDR campaigns.
- Local SEO agency prospecting — find businesses with low ratings or no website (= ideal upgrade targets).
- Review monitoring — pull recent Google reviews for your locations or competitors' on a schedule.
- Market sizing — count the total number of places in a category across a metro area before launching.
- Franchise expansion research — map every existing competitor location before picking new sites.
- Real-estate analytics — geocode all retail businesses in a corridor for foot-traffic modeling.
- Academic geography research — build datasets of public-facing local businesses for urbanism studies.
- Python 3.10+
- A free Apify account
- No Google Cloud Platform / Places API key required
git clone https://github.com/pro100chok/google-maps-leads-scraper-python.git
cd google-maps-leads-scraper-python
pip install -r requirements.txt
cp .env.example .env
# paste your APIFY_API_TOKEN
python main.pymain.py pulls up to 100 dental clinics in Austin, TX with their website-derived emails. Edit the constants at the top of the file to point at any city + business category combination.
| Field group | Notes |
|---|---|
| Identity | title, categories[], categoryName, address, city, state, countryCode, postalCode, lat/lng. |
| Contact | phone, phoneUnformatted, website, emails[] (from website), socials (Facebook, Instagram, X, LinkedIn, YouTube, TikTok, Telegram). |
| Status | permanentlyClosed, temporarilyClosed, claimThisBusiness (whether the listing is claimed). |
| Reviews | totalScore, reviewsCount, reviewsDistribution (5-star buckets), reviews[] (when includeReviews=true). |
| Images | images[] (when includeImages=true). |
| Operations | openingHours[] per day, popularTimesHistogram if Google has the data. |
| Misc | priceRange, menu, reservations, orderUrls, additionalInfo (category-specific attributes). |
- The actor sends the search terms + location (free text, structured city/state/country, or both) to Google Maps' internal
/maps/searchendpoints. - It paginates through the search result list (Google's UI caps at ~120 results — this is the wall everyone hits).
- If
deepSearch=true, when a search area returns ≥20 results, the actor recursively splits the bounding box into 4 sub-areas and re-queries each — repeating until each sub-area returns under the 20-place threshold. This lets you reliably pull every place in a city in one run. - For each place, it fetches the place-detail page to get the full record (hours, photos, attributes).
- If
scrapeContactsFromWebsite=trueand the place has a website, the actor visits the homepage plus contact / about pages and extracts emails + phones + social handles. - If
includeReviews=true, it paginates Google's internal review API in batches of 20 to attach up tomaxReviewsPerPlacereviews per place.
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_API_TOKEN"])
run = client.actor("pro100chok/google-maps-scraper").call(run_input={
"searchStringsArray": ["chiropractor"],
"locationQuery": "Denver, CO",
"maxCrawledPlacesPerSearch": 80,
"scrapeContactsFromWebsite": True,
"skipPlacesWithoutEmail": True,
})
for p in client.dataset(run["defaultDatasetId"]).iterate_items():
email = (p.get("emails") or [""])[0]
print(f"{p['title']:<40} {p['phone']:<18} {email}"){
"title": "Smile Bright Dentistry",
"address": "123 Example Blvd, Austin, TX 78701",
"lat": 30.2672, "lng": -97.7431,
"phone": "+1-512-555-0123",
"website": "https://smilebright.example",
"emails": ["info@smilebright.example", "hello@smilebright.example"],
"socials": {
"facebook": "https://facebook.com/smilebright",
"instagram": "https://instagram.com/smilebright"
},
"totalScore": 4.8,
"reviewsCount": 312,
"claimThisBusiness": true,
"categoryName": "Dentist",
"openingHours": [
{ "day": "Monday", "hours": "8AM–6PM" }
]
}| Parameter | Type | Required | Description |
|---|---|---|---|
searchStringsArray |
string[] | yes | Search terms (e.g. coffee shop, dental clinic). |
locationQuery |
string | no* | Free-text location (Berlin, Germany). |
city / state / country |
string | no* | Structured location fields (combined into one query). |
maxCrawledPlacesPerSearch |
integer | no | Cap per (term, location) pair. Default 100. 0 = unlimited with deepSearch. |
deepSearch |
boolean | no | Recursive geo-drilling to bypass Google's 120-place cap. Default false. |
categoryFilter |
string | no | Post-filter results by category substring (Barber shop). |
language |
string | no | hl parameter — language for the UI. Default en. |
countryCode |
string | no | gl parameter — 2-letter ISO country code. Default us. |
scrapeContactsFromWebsite |
boolean | no | Visit each place's website to extract emails + socials. Default true. |
skipPlacesWithoutEmail |
boolean | no | Drop places that have no extractable email. Default false. |
includeReviews |
boolean | no | Pull individual reviews per place. Default false. |
maxReviewsPerPlace |
integer | no | Cap on reviews per place. Default 20. |
includeImages |
boolean | no | Attach photo URLs per place. Default false. |
maxImagesPerPlace |
integer | no | Cap on images. Default 20. |
concurrency |
integer | no | Parallel workers during deep search. Default 3. |
maxRetries |
integer | no | Retries per request on transient failures. Default 5. |
* Either locationQuery or the city/state/country trio is required.
| File | Demonstrates |
|---|---|
examples/01_basic_usage.py |
Single-keyword + single-city search. |
examples/02_deep_search_bulk.py |
Geo-drilling to bypass the 120-place cap. |
examples/03_reviews_sentiment.py |
Star-rating distribution per place. |
examples/04_export_to_csv.py |
Multi-city plumber lead list with pandas filtering. |
examples/05_export_to_google_sheets.py |
Append leads into a shared Sheet for SDR teams. |
How much does it cost? The actor is metered per place returned. Apify's free $5 monthly credit covers a few thousand places. Optional add-ons (reviews, images) are billed separately at lower per-item rates.
How does this compare with the Google Places API? Google's Places API costs roughly $17 per 1,000 detailed lookups and does not return business emails, social-media handles, or claim-status flags. This actor includes contact extraction by default. It also has no per-day quota and no project-level approval requirements.
Will the data look like what I see on maps.google.com? Yes — the actor pulls from the same internal endpoints that power the Google Maps website. Ratings, review counts, photos, opening hours all match the live UI.
Why is the 120-place cap a thing?
Google Maps' search UI is designed for end-users browsing for a place, not for bulk enumeration. The pagination tops out at ~120 results regardless of how dense the area is. deepSearch works around it by automatically splitting the search area; no manual bounding-box config needed.
Does it work outside the US?
Yes — Google Maps coverage is global. Use countryCode to set the geolocation hint and language to pick the UI language. Most European countries, LATAM, and Asia work cleanly.
How accurate are the website-derived emails? The contact extractor runs the same homepage + contact-page crawl described in the Website Contact Scraper actor. About 60–85% of US local businesses have at least one extractable email; gyms, restaurants, retail are highest, attorneys and medical are lower (lots of contact forms instead of plain mailto).
Can I get emails for places without a website?
No — if the place doesn't link a website in its Google profile, there's no source to extract from. Filter those out with skipPlacesWithoutEmail: true.
Is this legal for cold outreach? You're scraping publicly-available data from Google Maps and the businesses' own websites. B2B cold email to those addresses is legal under most jurisdictions (CAN-SPAM in the US, legitimate-interest under GDPR in the EU) provided you offer an opt-out and identify yourself. Consult your own legal team for production-scale outreach.
- Website Contact Scraper — the contact-extraction stage as a standalone actor for non-Google inputs.
- Email Verifier — validate the emails before launching outreach.
- TripAdvisor Hotels & Reviews Scraper — for hospitality-specific data with reviews.
See all my actors at apify.com/pro100chok.
MIT — see LICENSE.
Built on top of the Google Maps Scraper Apify actor.