Google Maps Lead Finder & Business Enricher
Pricing
from $40.00 / 1,000 results
Google Maps Lead Finder & Business Enricher
Find local businesses on Google Maps and turn them into structured leads: website enrichment, public emails, phones, WhatsApp, social profiles, contact people, optional reviews with sentiment, and deterministic lead scoring. No AI or API keys required.
Pricing
from $40.00 / 1,000 results
Rating
0.0
(0)
Developer
code craker
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Share
Google Maps business discovery + website enrichment + public contact extraction + social discovery + deterministic lead scoring — in one Actor.
This Actor searches Google Maps for local businesses (for example "dentists in Johannesburg"), extracts each listing, removes duplicates across all your searches, visits each business's official website to collect publicly published emails, phone numbers, WhatsApp links, social profiles and team contacts, optionally reads public reviews with a transparent sentiment score, and finally scores every business as a sales/marketing lead with a documented, deterministic formula.
It is more than a basic Google Maps scraper: every record is a ready-to-use lead with contact channels, online-presence signals and observable marketing gaps.
V1 contains no AI. No OpenAI/Claude/Gemini, no LLM or embedding APIs, no API keys. Every value is extracted or computed deterministically, and nothing is fabricated.
Contents
- Who it is for
- Key features
- Use cases
- Input parameters
- Example input
- Output fields
- Example output
- Lead scoring methodology
- Review sentiment methodology
- Performance
- Proxy configuration
- Limitations
- Responsible usage
- Troubleshooting
- Local development
- Apify deployment
- Pricing considerations
- Architecture
Who it is for
- Sales teams and SDRs building prospect lists for a territory or vertical.
- Marketing and web agencies looking for businesses with gaps they can help with (no website, no Instagram, etc.).
- Local SEO consultants auditing a category in a city.
- CRM/RevOps teams enriching or refreshing local-business records.
- Researchers and analysts studying local markets.
Key features
| Feature | Details |
|---|---|
| Google Maps discovery | Any query + location, scrolls the results feed, handles consent pages, lazy loading, single-result redirects and "no results". |
| Rich listing data | Name, categories, rating, review count, full and parsed address, coordinates, phone, website, Place ID, CID, opening hours, price level, description, services/attributes, photo, Plus Code, business status, sponsored flag. |
| Cross-query deduplication | Place ID → normalized Google Maps URL → name + address → name + phone. A business found by "dentists" and "dental clinics" appears once, with both queries in matchedQueries. |
| Website enrichment | Crawls the official website (homepage + prioritized contact/about/team/services/locations pages, up to maxWebsitePages), respects robots.txt. |
| Emails | mailto: links, visible text and structured data; validated, false positives removed (image names, tracking ids, placeholders, no-reply), classified (general, contact, sales, support, bookings, marketing, other). |
| Phones | Google Maps listing phone plus website tel: links and text, normalized to E.164 with libphonenumber. National numbers are only converted when the country is known — the country code is never guessed. |
Detects wa.me, api.whatsapp.com/send, whatsapp://send links published by the business. | |
| Social profiles | Facebook, Instagram, LinkedIn, TikTok, YouTube, X/Twitter — only profiles explicitly linked from the official website; share buttons, posts and web-agency links are filtered out. |
| Contact people | Names and roles the business itself publishes (JSON-LD, team cards, "Name – Role" lines). A name is only accepted next to a role. |
| Reviews (optional) | Public Google Maps reviews with rating, text, relative date and reviewer display name. |
| Sentiment | Deterministic lexicon + star rating → per-review label, overallSentimentScore (0–100), recurring positive/negative topics. |
| Lead scoring | Transparent 0–100 score with a per-category breakdown and a HIGH/MEDIUM/LOW marketing opportunity level backed by observable facts. |
| Reliability | Retries with exponential backoff, timeouts at every stage, per-business error isolation (errors field), graceful CAPTCHA handling (no bypass), resumable after Apify migrations. |
Use cases
- Lead generation and sales prospecting by category and city
- Marketing agencies: find businesses with no website or weak social presence
- Local SEO audits of a category/area
- CRM enrichment of existing local-business accounts
- Market research and competitor mapping
- Dental lead generation – "dentists", "orthodontists", "dental clinics"
- Restaurant lead generation – "restaurants", "coffee shops", "caterers"
- Hotel lead generation – "hotels", "guest houses", "B&Bs"
- Car dealership lead generation – "car dealerships", "used car dealers"
- Real estate lead generation – "real estate agents", "property management"
- Business directory building for a region or niche
Input parameters
| Field | Type | Default | Description |
|---|---|---|---|
searchQueries | array of strings | — (required) | What to search for. Duplicates (case/whitespace-insensitive) are removed. 1–100 queries. |
location | string | null | Appended to every query as "<query> in <location>". Leave empty if queries already contain a location. |
maxResultsPerQuery | integer 1–500 | 20 | Google Maps results collected per query. Google usually shows up to ~120 per search. |
includeReviews | boolean | false | Extract public reviews and compute sentiment. |
maxReviewsPerBusiness | integer 0–200 | 10 | Review limit per business. |
includeSocialProfiles | boolean | true | Extract social profiles linked from the website. |
enrichWebsite | boolean | true | Visit official websites. When false, no website is requested. |
extractEmails | boolean | true | Extract emails from websites. |
extractPhones | boolean | true | Extract extra phones and WhatsApp links from websites. The Google Maps listing phone is always included. |
extractContactPeople | boolean | true | Extract names/roles published by the business. |
maxWebsitePages | integer 1–20 | 5 | Pages per website including the homepage. |
maxConcurrency | integer 1–50 | 10 | Parallel Google Maps pages. One browser per 5 pages; plan ~1 GB memory per 5 pages. |
proxyConfiguration | object | { "useApifyProxy": true } | Proxy for Google Maps. Default automatic (datacenter) group works and is not billed per GB; use RESIDENTIAL only if Google blocks the run. |
websiteProxyConfiguration | object | { "useApifyProxy": false } | Proxy for business websites only. Default none — website pages are cheap to fetch directly; use a DATACENTER group if sites block Apify IPs. |
Invalid input is rejected before any request is made, with every problem listed (e.g. "maxConcurrency" must be at least 1.).
Example input
{"searchQueries": ["dentists", "dental clinics"],"location": "Johannesburg, South Africa","maxResultsPerQuery": 100,"includeReviews": true,"maxReviewsPerBusiness": 10,"includeSocialProfiles": true,"enrichWebsite": true,"extractEmails": true,"extractPhones": true,"extractContactPeople": true,"maxWebsitePages": 5,"maxConcurrency": 5,"proxyConfiguration": { "useApifyProxy": true }}
Output fields
Every business is one dataset item with the same set of keys. Unavailable scalar values are null, unavailable lists are [].
| Group | Fields |
|---|---|
| Identity | businessName, category, categories, placeId, cid, googleMapsUrl, businessStatus, isSponsoredResult |
| Location | address, street, city, region, postalCode, country, latitude, longitude, plusCode |
| Listing | rating, reviewCount, openingHours, priceLevel, description, services, attributes, imageUrl (the listing's hero photo, when Google has loaded one), website |
| Contact | phone (E.164 when known), phones, internationalPhone, email, emails[] (email, type, matchesWebsiteDomain, sources), whatsapp, whatsappNumbers |
| Social | socialProfiles (facebook, instagram, linkedin, tiktok, youtube, twitter, whatsapp) |
| People | contactPeople[] (name, role, sourceUrl) |
| Website | websiteData (title, description, pagesCrawled, addresses, additionalLocations, signals: https, reachable, hasContactPage, hasDescription, mobileViewport, structuredData) |
| Reviews | reviews[] (reviewId, rating, text, reviewDate, reviewerName, reviewUrl, sentiment), overallSentimentScore, topPositiveTopics, topNegativeTopics |
| Scoring | leadScore, leadScoreBreakdown, marketingOpportunity, marketingOpportunities |
| Run info | enrichmentStatus (website: success/failed/skipped/blocked/no-website/disabled; reviews: success/failed/skipped/not-available), errors[] (error, errorStage), queryUsed, matchedQueries, searchPosition, scrapedAt |
A RUN_SUMMARY record is also saved to the key-value store (results per query, duplicates skipped, websites crawled/failed, whether Google blocked any request).
The dataset has three Console views: Leads overview, Contacts & social, and Lead scoring.
Example output
An abbreviated record from a real run ("dentists in Johannesburg, South Africa", website enrichment and reviews enabled):
{"businessName": "Houghton Dental","category": "Dentist","categories": ["Dentist"],"rating": 5,"reviewCount": 147,"address": "53 2nd Avenue, The Houghton Medical Suites Gate 2, The Houghton Hotel, Houghton Estate, Johannesburg, 2198, South Africa","street": "53 2nd Avenue","city": "Johannesburg","region": null,"postalCode": "2198","country": "South Africa","latitude": -26.1674613,"longitude": 28.0743136,"phone": "+27748040271","phones": ["+27748040271", "+27110834693"],"internationalPhone": "+27 74 804 0271","email": "smile@houghtondental.co.za","emails": [{ "email": "smile@houghtondental.co.za", "type": "other", "matchesWebsiteDomain": true, "sources": ["https://www.houghtondental.co.za/", "https://www.houghtondental.co.za/book/"] }],"whatsapp": "https://wa.me/27748040271","whatsappNumbers": ["+27748040271"],"website": "http://www.houghtondental.co.za","googleMapsUrl": "https://www.google.com/maps/search/?api=1&query=Houghton%20Dental&query_place_id=ChIJO7kvs7wNlR4R0cnV1vKyF84","placeId": "ChIJO7kvs7wNlR4R0cnV1vKyF84","cid": "14850535052333664721","openingHours": { "Monday": "8 am–5 pm", "Saturday": "8 am–1 pm", "Sunday": "Closed" },"priceLevel": null,"socialProfiles": {"facebook": "https://www.facebook.com/houghtondental1","instagram": "https://www.instagram.com/houghtondental","linkedin": null,"tiktok": null,"youtube": null,"twitter": null,"whatsapp": "https://wa.me/27748040271"},"contactPeople": [],"reviews": [{ "reviewId": "Ci9DQUlRQUNv…", "rating": 5, "text": "If you're considering cosmetic dentistry, I cannot recommend…", "reviewDate": "2 months ago", "reviewerName": "…", "reviewUrl": null, "sentiment": "positive" }],"overallSentimentScore": 98,"topPositiveTopics": ["atmosphere", "communication", "service", "staff", "professionalism"],"topNegativeTopics": [],"leadScore": 79,"leadScoreBreakdown": { "contactability": 30, "onlinePresence": 15, "socialPresence": 6, "reviewStrength": 18, "marketingOpportunity": 10 },"marketingOpportunity": "HIGH","marketingOpportunities": ["No LinkedIn profile linked from the website","No TikTok profile linked from the website","No YouTube profile linked from the website","No X/Twitter profile linked from the website","Business has more than 100 Google reviews","High Google rating (5)","Public phone number available","Public email address available","Public WhatsApp contact available"],"enrichmentStatus": { "website": "success", "reviews": "success" },"errors": [],"queryUsed": "dentists","matchedQueries": ["dentists"],"searchPosition": 1,"scrapedAt": "2026-09-13T17:06:18.115Z"}
Lead scoring methodology
leadScore (0–100) is the sum of five categories. It summarizes how reachable and how visible a business is, plus observable gaps. It is not a prediction of conversion, revenue or buying intent.
| Category | Max | Points |
|---|---|---|
| Contactability | 30 | Phone 12 · Email 12 · WhatsApp 6 |
| Online presence | 20 | Website listed 8 · HTTPS 3 · Website loaded successfully 4 · Contact page found 2 · Meaningful description 3 |
| Social presence | 15 | Facebook 3 · Instagram 3 · LinkedIn 3 · TikTok 2 · YouTube 2 · X/Twitter 2 |
| Review strength | 20 | Rating: ≥4.5 → 10, ≥4.0 → 8, ≥3.5 → 5, ≥3.0 → 3, lower → 1, none → 0. Review count: ≥200 → 10, ≥100 → 8, ≥50 → 6, ≥20 → 4, ≥5 → 2, ≥1 → 1 |
| Marketing opportunity | 15 | No website 5 · 4+ social channels missing 4 (2–3 missing: 2) · Strong Maps presence (rating ≥4.0 and ≥50 reviews) 3 · Contactable by phone/email/WhatsApp 3 |
Marketing opportunity level: opportunity points ≥10 → HIGH, ≥5 → MEDIUM, otherwise LOW.
marketingOpportunities lists the observable facts behind the level, e.g. "No website detected on the Google Maps listing", "No Instagram profile linked from the website", "Business has more than 100 Google reviews", "Public phone number available".
Notes:
- Missing social channels are only counted and reported when the website was actually crawled and social extraction was enabled. With
enrichWebsite: false, the Actor makes no claims about social profiles. - A missing website lowers online presence but raises marketing opportunity on purpose: the two categories answer different questions.
- The score depends on data Google and the website expose at run time, so it can differ between runs.
Review sentiment methodology
No AI and no external service — a transparent word-list approach:
- Text score (−1…1): counts positive words (e.g. excellent, friendly, professional) and negative words (e.g. rude, dirty, overpriced), with simple negation handling ("not good" counts as negative).
- Review score = 70% star rating (
(rating − 3) / 2) + 30% text score; text only when there is no rating. - Label: > 0.25 →
positive, < −0.25 →negative, otherwiseneutral. overallSentimentScore= mean review score mapped to 0–100 (nullwithout reviews).- Topics: a keyword dictionary (staff, service, professionalism, price, wait time, cleanliness, booking, communication, quality, location, atmosphere, food). The most frequent topics in positive and negative reviews become
topPositiveTopics/topNegativeTopics.
Performance
Measured on the Apify platform (September 2026, Johannesburg dentists, website enrichment on):
| Run | Proxy | Reviews | Memory / concurrency | Leads | Time | Cost | Per lead |
|---|---|---|---|---|---|---|---|
| A | Apify Proxy, automatic (datacenter) | off | 4 GB / 5 | 26 | 5.5 min | $0.08 | $0.003 |
| B | Apify Proxy, RESIDENTIAL | on (5 each) | 4 GB / 10 | 16 | 6.2 min | $0.23 | $0.015 |
| C | Apify Proxy, RESIDENTIAL | on (5 each) | 2 GB / 5 | 5 | 2.5 min | $0.11 | $0.022 |
Where the money goes:
- Compute is roughly constant: about $0.003–0.005 per lead (≈ 0.02 compute units), regardless of memory — doubling memory doubles the CPU share and halves the run time at the same cost.
- Residential proxy bandwidth ($8/GB) is the only expensive input. The Actor keeps it low by blocking map tiles, avatars, fonts and telemetry at the browser level while keeping the HTTP cache, so the multi-megabyte Google Maps app is downloaded once per browser, not once per page. Fixed cost is ≈ 4 MB per browser plus ≈ 3 MB per search; each additional business is only ≈ 0.1–0.3 MB. Small runs therefore look expensive per lead; a 100-lead run costs ≈ 30–40 MB (≈ $0.30).
- Datacenter proxy traffic is not billed per GB, which is why run A is 5× cheaper than run B. Business websites are fetched without a proxy by default (see
websiteProxyConfiguration).
Speed: the work is browser-bound, so throughput scales with CPU, and on Apify CPU scales with memory (4 GB ≈ 1 CPU). Rules of thumb after ~30 s of start-up: ≈ 4 s per business without reviews and ≈ 8 s with reviews per 4 GB of memory (e.g. 100 leads with reviews ≈ 10–13 min at 4 GB, ≈ 6–7 min at 8 GB with maxConcurrency 20). Options that add time: includeReviews (largest), maxWebsitePages, very large maxResultsPerQuery (feed scrolling).
Cheapest configuration: default proxy (automatic), includeReviews: false, maxWebsitePages: 3.
Proxy configuration
- Default:
{ "useApifyProxy": true }(automatic datacenter group). In our tests Google Maps served full results through it, and datacenter traffic is not billed per GB — start here. - If Google blocks the run (CAPTCHA, "unusual traffic",
RUN_SUMMARY.blockedByGoogle) or hides reviews behind its limited view, switchproxyConfigurationto{ "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "ZA" }(pick the country of your searches). Residential bandwidth costs about $8/GB; see Performance for what that means per lead. - Business websites use
websiteProxyConfiguration, default no proxy (cheap, direct requests). Sites that answer with a bot-protection challenge are automatically retried once through the Google Maps proxy; the few that still block are reported asenrichmentStatus.website: "blocked". Set a DATACENTER or RESIDENTIAL group here only if many sites in your market block direct requests. - Custom proxies:
{ "useApifyProxy": false, "proxyUrls": ["http://user:pass@host:port"] }. Credentials come only from input and are never logged. - Running locally without an Apify token, the Actor continues without a proxy and logs a warning.
- The Actor never tries to solve or bypass CAPTCHAs. Session rotation is capped and blocked requests are not retried endlessly.
Limitations
- Google Maps "limited view". Google may serve signed-out visitors a reduced page without review counts, the Reviews tab or some About sections. When that happens,
reviewCountisnullandenrichmentStatus.reviewsisnot-available— values are never estimated. In our tests this varied from page load to page load. - Google result caps. A single search usually lists up to ~120 places. Use narrower queries or locations (e.g. suburbs) for broader coverage.
- Selectors can change. Google changes its interface frequently. Extraction relies on stable attributes (
data-item-id, roles, ARIA labels) with fallbacks, but fields may temporarily go missing after a Google update. - JavaScript-only websites. Website enrichment uses fast HTTP requests without a browser. Sites that render their content only with JavaScript may yield fewer emails, services or people.
- Obfuscated emails (e.g. Cloudflare email protection, images of emails) are intentionally not decoded.
- Address parsing splits the address Google displays. Components that cannot be identified confidently (often
region) arenull. - Review dates are Google's relative dates ("2 months ago").
reviewUrlisnullbecause Google does not expose a stable public link per review. - Contact people are only found when a business publishes names next to roles; many small-business sites do not.
- CAPTCHA/blocking: if Google blocks the environment, affected searches are reported in
RUN_SUMMARYand the run fails with a clear message if nothing could be extracted. - Very large runs keep their progress state in the key-value store for migration recovery. For tens of thousands of businesses with reviews, split the work into several runs (e.g. per suburb or query group).
- The lead score is a heuristic summary, not a conversion prediction.
Responsible usage
This Actor collects only publicly accessible business information: Google Maps listings, public reviews, and pages a business publishes on its own website.
It does not:
- bypass authentication, CAPTCHAs, paywalls or other security controls,
- access private accounts or logged-in content,
- use leaked or purchased data,
- perform people-search or personal-data enrichment,
- guess, generate or fabricate emails, phone numbers or social profiles.
It respects robots.txt on business websites and limits how many pages it crawls per site.
You are responsible for using the data lawfully. In particular:
- Comply with the privacy and marketing laws that apply to you and your recipients (e.g. GDPR, POPIA in South Africa, CAN-SPAM, PECR, CASL) before contacting anyone, and honour opt-outs.
- Review names in
contactPeopleandreviews[].reviewerNameas personal data. DisableextractContactPeopleorincludeReviewsif you do not need them. - Respect Google's and websites' terms of service and keep request volumes reasonable.
Troubleshooting
| Symptom | What to do |
|---|---|
Invalid input: ... | Fix the listed fields; every problem is reported at once. |
| Run fails with "Google Maps blocked the requests" | Use Apify Proxy RESIDENTIAL, lower maxConcurrency, try again later. Check RUN_SUMMARY.searchQueries[].status. |
Fewer results than maxResultsPerQuery | Google listed fewer places for that search. Use more specific locations or additional queries. |
reviewCount/reviews empty with enrichmentStatus.reviews: "not-available" | Google served its limited view. Try another proxy group or country. |
enrichmentStatus.website: "failed" | See errors[], e.g. "Website request timed out". The site was down, too slow or rejected the request. |
enrichmentStatus.website: "skipped" | Website disallows crawling in robots.txt, or the listed website is a social profile. |
enrichmentStatus.website: "blocked" | The site answered with a bot-protection challenge (Cloudflare etc.) even after the retry through the Google Maps proxy. Set websiteProxyConfiguration to a RESIDENTIAL group for those runs. |
| No emails although the site shows one | The email may be rendered by JavaScript, shown as an image or obfuscated. |
| Memory errors | Lower maxConcurrency or increase Actor memory. |
Set the environment variable CRAWLEE_LOG_LEVEL=DEBUG for detailed logs.
Local development
Requirements: Node.js 20+ (24 recommended), npm, optionally Docker.
npm installnpx playwright install chromium # browser for local runs and browser testsnpm run build # compile TypeScript to dist/npm test # unit + mocked integration tests (no network)npm run test:browser # page-script tests in headless Chromium (no network)npm run lint # ESLint + type check
Run the Actor locally. Input is read from storage/key_value_stores/default/INPUT.json:
mkdir -p storage/key_value_stores/defaultcat > storage/key_value_stores/default/INPUT.json <<'JSON'{ "searchQueries": ["dentists"], "location": "Johannesburg, South Africa", "maxResultsPerQuery": 5 }JSONnpm run start:dev # build + run; results in storage/datasets/default/
Or with the Apify CLI: apify run.
Smoke tests (live Google Maps, tiny configuration):
npm run smoke # builds and runs dist/main.js, then verifies the datasetnpm run smoke:docker # builds the Docker image and runs the same checks inside it
The smoke test exits with code 2 (not success) when Google blocks the environment, so a block is never reported as a pass.
Docker (the Apify Playwright base image is linux/amd64; Apple Silicon machines run it under emulation):
docker build --platform linux/amd64 -t google-maps-lead-finder .docker run --rm --platform linux/amd64 \-v "$PWD/storage:/home/myuser/storage" -e CRAWLEE_STORAGE_DIR=/home/myuser/storage \google-maps-lead-finder
No environment variables are required. See .env.example for optional ones.
Apify deployment
- Install the CLI and log in:
npm install -g apify-cliandapify login. - From the project directory run
apify push. The platform builds the Dockerfile (apify/actor-node-playwright-chrome:24-1.63.0). - In Apify Console open the Actor → Input, enter queries and a location, keep the default proxy (switch to RESIDENTIAL only if Google blocks the run), and click Start.
- Default run memory is 4096 MB (
maxConcurrency10). Use 8192 MB withmaxConcurrency20 for faster large runs; increase the timeout for very large runs. - Export results from the Storage → Dataset tab (JSON, CSV, Excel), or use the API or integrations.
Alternatively, connect the Git repository in Console (Source: Git repository) so every push rebuilds the Actor.
Pricing considerations
Measured platform cost is ≈ $3 per 1,000 leads with the default datacenter proxy and website enrichment, and ≈ $8–15 per 1,000 with residential proxy and reviews (see Performance).
Suggested Store pricing (starting points):
- Pay per result: $5 per 1,000 leads for the base run, which still leaves margin on the datacenter configuration. If you want one price that also covers residential/reviews runs, $10 per 1,000.
- Pay per event (needs
Actor.charge()calls, not in V1): ≈ $0.003 per lead discovered + $0.002 per website enriched + $0.003 per business with reviews.
Re-measure with RUN_SUMMARY and the run's usage tab after any Google or platform change.
Architecture
src/├── main.ts Actor entry: init, input, proxy, run, exit status├── input.ts zod validation, defaults, query dedupe├── proxy.ts Apify/custom proxy configuration├── pipeline/│ ├── run.ts discovery → website enrichment → output (resumable phases)│ ├── state.ts persisted run state (survives migrations)│ └── output.ts lead building, post-processor hook, dataset push├── google-maps/│ ├── crawler.ts PlaywrightCrawler: search feed, place pages, reviews, consent, blocking│ ├── page-scripts.ts in-browser DOM readers (raw strings only)│ ├── parser.ts pure parsing: rating, reviews, address, hours, IDs│ └── types.ts├── website/│ ├── crawler.ts CheerioCrawler with per-site page budget, robots.txt│ ├── parser.ts page extraction, link prioritization, merging│ ├── html.ts Cheerio loader│ └── types.ts├── enrichment/ email.ts · phone.ts (incl. WhatsApp) · social.ts · contact-people.ts├── reviews/sentiment.ts deterministic sentiment + topics├── scoring/lead-score.ts lead score, opportunity level and facts├── lead/ output record type + builder└── utils/ normalize.ts · deduplicate.ts · urls.ts · errors.ts
Extensibility: LEAD_POST_PROCESSORS in src/pipeline/output.ts receives each finished, deterministic LeadRecord and can add fields. V1 registers none. Future versions can add optional processors (e.g. summaries or qualification) without touching discovery, enrichment or scoring.