Google Maps Lead Finder & Business Enricher avatar

Google Maps Lead Finder & Business Enricher

Pricing

from $40.00 / 1,000 results

Go to Apify Store
Google Maps Lead Finder & Business Enricher

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

code craker

Maintained by Community

Actor 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

  • 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

FeatureDetails
Google Maps discoveryAny query + location, scrolls the results feed, handles consent pages, lazy loading, single-result redirects and "no results".
Rich listing dataName, 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 deduplicationPlace 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 enrichmentCrawls the official website (homepage + prioritized contact/about/team/services/locations pages, up to maxWebsitePages), respects robots.txt.
Emailsmailto: 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).
PhonesGoogle 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.
WhatsAppDetects wa.me, api.whatsapp.com/send, whatsapp://send links published by the business.
Social profilesFacebook, 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 peopleNames 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.
SentimentDeterministic lexicon + star rating → per-review label, overallSentimentScore (0–100), recurring positive/negative topics.
Lead scoringTransparent 0–100 score with a per-category breakdown and a HIGH/MEDIUM/LOW marketing opportunity level backed by observable facts.
ReliabilityRetries 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

FieldTypeDefaultDescription
searchQueriesarray of strings— (required)What to search for. Duplicates (case/whitespace-insensitive) are removed. 1–100 queries.
locationstringnullAppended to every query as "<query> in <location>". Leave empty if queries already contain a location.
maxResultsPerQueryinteger 1–50020Google Maps results collected per query. Google usually shows up to ~120 per search.
includeReviewsbooleanfalseExtract public reviews and compute sentiment.
maxReviewsPerBusinessinteger 0–20010Review limit per business.
includeSocialProfilesbooleantrueExtract social profiles linked from the website.
enrichWebsitebooleantrueVisit official websites. When false, no website is requested.
extractEmailsbooleantrueExtract emails from websites.
extractPhonesbooleantrueExtract extra phones and WhatsApp links from websites. The Google Maps listing phone is always included.
extractContactPeoplebooleantrueExtract names/roles published by the business.
maxWebsitePagesinteger 1–205Pages per website including the homepage.
maxConcurrencyinteger 1–5010Parallel Google Maps pages. One browser per 5 pages; plan ~1 GB memory per 5 pages.
proxyConfigurationobject{ "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.
websiteProxyConfigurationobject{ "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 [].

GroupFields
IdentitybusinessName, category, categories, placeId, cid, googleMapsUrl, businessStatus, isSponsoredResult
Locationaddress, street, city, region, postalCode, country, latitude, longitude, plusCode
Listingrating, reviewCount, openingHours, priceLevel, description, services, attributes, imageUrl (the listing's hero photo, when Google has loaded one), website
Contactphone (E.164 when known), phones, internationalPhone, email, emails[] (email, type, matchesWebsiteDomain, sources), whatsapp, whatsappNumbers
SocialsocialProfiles (facebook, instagram, linkedin, tiktok, youtube, twitter, whatsapp)
PeoplecontactPeople[] (name, role, sourceUrl)
WebsitewebsiteData (title, description, pagesCrawled, addresses, additionalLocations, signals: https, reachable, hasContactPage, hasDescription, mobileViewport, structuredData)
Reviewsreviews[] (reviewId, rating, text, reviewDate, reviewerName, reviewUrl, sentiment), overallSentimentScore, topPositiveTopics, topNegativeTopics
ScoringleadScore, leadScoreBreakdown, marketingOpportunity, marketingOpportunities
Run infoenrichmentStatus (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.

CategoryMaxPoints
Contactability30Phone 12 · Email 12 · WhatsApp 6
Online presence20Website listed 8 · HTTPS 3 · Website loaded successfully 4 · Contact page found 2 · Meaningful description 3
Social presence15Facebook 3 · Instagram 3 · LinkedIn 3 · TikTok 2 · YouTube 2 · X/Twitter 2
Review strength20Rating: ≥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 opportunity15No 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:

  1. 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).
  2. Review score = 70% star rating ((rating − 3) / 2) + 30% text score; text only when there is no rating.
  3. Label: > 0.25 → positive, < −0.25 → negative, otherwise neutral.
  4. overallSentimentScore = mean review score mapped to 0–100 (null without reviews).
  5. 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):

RunProxyReviewsMemory / concurrencyLeadsTimeCostPer lead
AApify Proxy, automatic (datacenter)off4 GB / 5265.5 min$0.08$0.003
BApify Proxy, RESIDENTIALon (5 each)4 GB / 10166.2 min$0.23$0.015
CApify Proxy, RESIDENTIALon (5 each)2 GB / 552.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, switch proxyConfiguration to { "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 as enrichmentStatus.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, reviewCount is null and enrichmentStatus.reviews is not-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) are null.
  • Review dates are Google's relative dates ("2 months ago"). reviewUrl is null because 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_SUMMARY and 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 contactPeople and reviews[].reviewerName as personal data. Disable extractContactPeople or includeReviews if you do not need them.
  • Respect Google's and websites' terms of service and keep request volumes reasonable.

Troubleshooting

SymptomWhat 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 maxResultsPerQueryGoogle 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 oneThe email may be rendered by JavaScript, shown as an image or obfuscated.
Memory errorsLower 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 install
npx playwright install chromium # browser for local runs and browser tests
npm 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/default
cat > storage/key_value_stores/default/INPUT.json <<'JSON'
{ "searchQueries": ["dentists"], "location": "Johannesburg, South Africa", "maxResultsPerQuery": 5 }
JSON
npm 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 dataset
npm 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

  1. Install the CLI and log in: npm install -g apify-cli and apify login.
  2. From the project directory run apify push. The platform builds the Dockerfile (apify/actor-node-playwright-chrome:24-1.63.0).
  3. 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.
  4. Default run memory is 4096 MB (maxConcurrency 10). Use 8192 MB with maxConcurrency 20 for faster large runs; increase the timeout for very large runs.
  5. 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.