Turn Google Maps searches into CRM-ready local business leads: emails, websites, socials, booking links, unclaimed-profile detection, 0-100 lead scores, dedup & new-lead tracking.
1.9.1 (2026-09-29): A call with no search returns a small demo
An empty call no longer fails. Starting the Actor with no search term, no start URL and no location (as an AI agent's test call or an API probe does) used to stop with "Missing input". It now searches "dentists in Austin, Texas" and returns up to 10 leads (about $0.03), and says so in the log. Any call that names a search, a URL or a location behaves exactly as before, including the "Missing input" message when only some of the fields are filled in wrong.
1.9.0 (2026-09-28): Faster runs, same leads
Runs finish in roughly half the time with the same data in every field. On three test searches of 20 leads each, cloud runs took 83–130 seconds instead of 153–256. Nothing in the output changed: same fields, same order, same best-first sorting.
Changed
Websites are read while the browser keeps working. Before, the browser waited on each business website (up to five pages, sometimes 30 seconds for a slow site) before it opened the next Google Maps place. Website reading now runs in the background, up to 8 sites at a time, and the browser goes straight on to the next place. enrichmentConcurrency also sets this number.
maxResults is still exact. The Actor counts leads whose website is still being read, stops opening places once saved plus pending leads reach maxResults, and opens more if a filter that runs after the website check (onlyMissingEmail, onlyMissingBookingLink) drops one. It never saves more than maxResults leads, also in lite mode.
Your maximum charge is respected up front. When the "maximum cost per run" covers fewer leads than maxResults, the run stops at that number instead of crawling places it cannot deliver.
A run that is stopped early (migration or abort) saves the leads whose website was still being read, with their Google Maps data.
A website that is still not answering after 150 seconds no longer holds up the run: that lead ships with its Google Maps data.
Fixed
The log no longer warns "Attempting to charge for an unknown event" for every lead. Those charge calls did not match the Actor's pricing and never billed anything. The only charge is still one per lead saved to the dataset, and RUN_SUMMARY.billableEvents now shows that count.
1.8.0 (2026-09-27): Every documented field ships, and addresses are clean
An audit of 40 real records found that the README promised fields the dataset never contained. Since 1.5 ("Lean output") the Actor still computed them but no longer copied them onto the record. This release puts them back, fixes the address parser, and corrects the README.
Added (appended after the existing keys: no field was renamed or removed, and existing columns keep their order)
Google Maps details:categories, openNow, openingHours, priceLevel, attributes, serviceAreaBusiness.
Auditable score:reachabilityScore, reputationScore, completenessScore (the three sub-scores behind leadScore), plus reachableChannels and human-readable opportunitySignals.
Website enrichment:websiteResolvedUrl, contactPageUrl, bookingLinks (the URLs behind bookingLinkFound, with http/https/www variants of the same URL listed once), and flat linkedinUrl / instagramUrl / facebookUrl, each the first profile from the unchanged nested socialLinks.
Provenance and metadata:fieldSources (maps / website / derived for every non-empty field; city/state/country filled from your locations input are marked derived), searchQuery, scrapedAt.
Detail mode (the default) fills all of the above. Lite mode (skipPlaceDetailPages: true) leaves plusCode, openingHours, priceLevel, attributes, serviceAreaBusiness and claimedByOwner empty, and its card addresses are street-only. The README now has a per-mode field table.
Fixed
Map button labels in address. The phone on a search-result card was taken as the address with its button labels glued on, e.g. "(720) 298-1900WebsiteDirections" for a Denver plumber, which left city/state/country empty. Card segments now drop trailing button labels ("Website", "Directions", "Call", "Share", ...) and glued phone numbers, and a segment is only an address if something address-like remains. Service-area businesses with no public address now get address: null instead of UI text. Every record's address also goes through the same cleaning before it ships.
US phones without an address. North American numbers shown as (720) 298-1900 keep their +1, so service-area businesses and search-card leads (lite mode, salvaged leads) get an E.164 phone even when there is no address to infer the country from.
Postal codes beside the city name now parse: 10117 Berlin, London SW1A 2AA, Sydney NSW 2000 (the city and state are split as well).
No more street pieces as city. A one-segment search-card address (3636 E Anne St A, 6201 Broadway) used to give city: "E Anne St A" / "Broadway", and a building name before the street (One Biscayne Tower, 2 Biscayne Blvd…) used to become the city. The first segment is now the city only in City, ST 12345 or City, Country forms. street is the first segment with a house number, so card addresses now fill street.
A card blurb glued after the street (2030 W Desert Cove AveVeteran retailer of home water systems) is cut off.
locations fallback: "Austin, Texas" no longer yields country: "Texas". A US state name or code becomes state, and the country becomes United States.
openingHours no longer includes the review-histogram rows ("5", "4", ...) or Google's trailing icon glyph.
Search cards (lite mode and salvaged leads). Google now repeats the business name at the start of each card (Austin City DentalAustin City Dental5.0…), which hid the category, rating prefix and review count. Every leading copy is now stripped. On the 2026-09-27 lite benchmark, category fill went from 0% to 100%, and review counts are read whenever the card shows them. A status word glued to the address (7000 Broadway Ste 205Open) is also removed, and a glued status ("1st StOpen ⋅ Closes 5 PM") now sets openNow.
serviceAreaBusiness: a place page that loaded but shows no address (Google hides it for service-area businesses) is now flagged true. Before, it relied on "service area" text the headless view rarely prints.
includeRawPlaceData: true now adds the documented rawPlaceData diagnostics object to each row. It had been computed but never shipped. It is off by default, so default output is unchanged.
Docs
The README said lite mode was the default. Detail mode has been the default since 1.3 (skipPlaceDetailPages: false), which is why runs take several seconds per lead. The README now says so, and it lists which fields each mode fills.
The example record and the benchmark table are regenerated from real runs of this version.
1.7.3 (2026-07-01) — Always finish before the run timeout
Fixed
Runs can no longer be killed by their own timeout. The Actor now reads the run's hard timeout (ACTOR_TIMEOUT_AT) and wraps up gracefully before it: crawling stops early, every collected lead is flushed (best-first as usual), history is persisted, and the run exits SUCCEEDED with partial results instead of FAILED/timed-out. This also fixes Apify's automated quality test (prefilled input, 5-minute limit), which had flagged the Actor as "Under maintenance".
Google rate-limit cooldowns are capped to the remaining time budget — a 2-minute backoff can no longer sleep the run into its timeout.
Website-enrichment requests started near the deadline get proportionally shorter timeouts (and no retries), so a slow site can't outlive the run; leads still ship Maps-only if enrichment has to be skipped.
A run where Google blocked every request now finishes with a clear ⚠️ warning status instead of hard-failing — rate limits are temporary and nothing is charged, so schedules/integrations can simply retry.
Changed
Prefilled maxResults lowered 50 → 25 for a faster, cheaper first-run preview (benchmarked at under 3 minutes fully enriched). Production runs can raise it up to 5000 as before.
On short-timeout runs (under 10 minutes) per-page navigation and handler timeouts are tightened so one slow page can't eat the whole budget.
Leads are now written best-first: the dataset is ordered by leadScore descending (ties broken by review count, then name), so every view — 💎 Leads overview, 📞 Contact data, 🎯 Opportunities — opens with the most valuable prospects on top. (Apify dataset views can't sort by field, so order is controlled at write time.)
Leads are written in a single sorted batch at the end of the run (also fewer dataset API calls than per-lead writes), and flushed on migration/abort so nothing is lost.
Changed
Faster website enrichment: a business site's internal pages (contact/about/booking) are now fetched concurrently instead of one-by-one, cutting per-lead enrichment latency from the sum of page times to the slowest single page. Same pages visited, identical output.
Refreshed the Contact data and Opportunities dataset views to show the lean v1.5 fields (e.g. leadScore, decisionMakerHint, websiteDomain) and dropped references to removed columns.
1.6 (2026-06-14) — Decision-maker name cleanup
Fixed
decisionMakerHint.name no longer leaks section words: greedy name capture was gluing a leading nav/heading word onto the name (e.g. Experience Mike Meek, Team Bruce Jay, Reviews Alyssa Dawson, Alignment Vincent Ip). Names are now refined:
leading section/navigation words are stripped;
a name re-anchors on an honorific that appears mid-capture (Tucker Dr. Tucker → Dr. Tucker);
a three-word capture is trimmed to its last two words unless the first word is an honorific, the middle word is an initial (John M. Smith), or the middle word is a surname particle (Steven Van Wicklen) — this catches arbitrary section words not in the denylist;
brand/abbreviation tokens (Sunbit. Find) are rejected rather than surfaced as a person.
8 new unit tests cover these cases (70 tests total).
Lead-targeting filters in the UI: minRating / maxRating, minReviewCount, skipClosedPlaces, requireReachableChannel, onlyUnclaimed, onlyMissingWebsite, onlyMissingEmail, onlyMissingBookingLink, onlyNewLeads.
Enrichment controls in the UI: enrichWebsites, genericEmailsOnly, extractSocialLinks, detectBookingLinks, maxPagesPerWebsite.
Inputs are grouped into Search / Filter / Enrichment sections. Still no dropdowns (checkboxes and numbers only); vertical and speed stay automatic.
1.3 (2026-06-14) — One-decision input
Changed
Radically simplified input: just searchTerms, locations, maxResults, and optional startUrls. Removed both dropdowns (vertical, speedMode) and every checkbox/filter from the UI — the dropdowns were also rendering unreliably on Apify.
Detail mode is now the default (skipPlaceDetailPages: false) — maximum data per lead (phone, full address incl. state, attributes, open-now, claimed status). Benchmarking showed detail mode runs block-free proxy-free.
Internal crawl concurrency raised 2 → 3 (≈50% faster, still block-free in testing).
The history key for new-lead tracking is derived automatically from the query, so monitoring works across re-runs with zero configuration.
The vertical scoring template is resolved per business from its category (no input needed).
All enrichment (emails, socials, booking, decision-maker, tech stack), the reachability gate, and closed-business filtering are simply always on. Power-user flags remain accepted via the API/JSON (see README).
1.2 (2026-06-14) — Sales-ready leads
Added
Hard reachability gate (requireReachableChannel, default on): no record ships without a phone, email, or website. Place-only rows are dropped (droppedUnreachable in RUN_SUMMARY).
E.164 phone normalization via libphonenumber-js, country/region-aware (e.g. (512) 451-8256 → +15124518256). Unparseable numbers are null, never guessed.
Per-field source tagging (fieldSources): every field is tagged maps, website, or derived so a buyer can trust each value.
Auditable lead score: leadScore is now a documented blend of three exposed sub-scores — reachabilityScore, reputationScore, completenessScore.
Decision-maker hint (decisionMakerHint): name + title (Owner/Founder/DDS/…) derived from About/Team pages, conservative (null unless confident).
Resolved website URL (websiteResolvedUrl): the final URL after redirects, distinct from the Maps-listed link.
State/region split out of the address (state), alongside street/city/postal/country.
Detail-page signals: attributes (wheelchair, LGBTQ+ friendly, women-owned, …), openNow, serviceAreaBusiness, claimedByOwner, structured hoursStructured when a full week is present.
Top-level linkedinUrl / instagramUrl / facebookUrl mirrors and reachableChannels for quick CRM mapping.
reachableChannels, categories (primary + any secondary exposed), and reachabilityScore/reputationScore/completenessScore columns.
enrichmentConcurrency input to tune website-crawl parallelism (default 12) for maximum speed.
Fixed
Booking links mis-detected as websites: US feed cards expose a "Book online" (rwg_token) link, not the website. Website-vs-booking is now classified in the feed, so the website/email enrichment is no longer polluted by booking-provider URLs.
Coordinate poisoning: the og:image static-map center reflects the viewer's IP under Google's limited view (it returned Spain for an Austin clinic). Coordinates now come only from the place URL's !3d!4d.
Attribute extraction no longer captures the business name or generic UI labels (e.g. "Reviews for X").
Address parsing no longer mistakes a street number ("4601 N Lamar Blvd") for a postal code on partial feed addresses; the postal token must dominate its segment.
Country is inferred as "United States" from a US ST 12345 pattern, which Google omits when the viewer is also in the US.
Vertical (scoring template + suggested offer) is now resolved per business from its category, so mixed-query runs label each lead correctly.
Honest limitations (documented, not faked)
Under Google's headless "limited view" (which a proxy-free run receives), reviewCount is intermittent and ratingDistribution, reviewVelocity90d, popularTimes, photoCount, secondary categories, and yearsOnMaps are not reliably exposed. These are emitted as null rather than guessed. Full reliability for those fields requires residential proxies (full view) or the Maps protobuf API path.
Detail-page fields (state, attributes, openNow, claimedByOwner, full address) require detail mode; lite mode populates what the search card exposes and nulls the rest.
1.1 (2026-06-11)
Added
Unclaimed Google Business Profile detection (unclaimedGoogleProfile) + onlyUnclaimed filter — the highest-intent signal for local SEO outreach.
Closed-business detection (businessStatus) with skipClosedPlaces filter (on by default).
Multiple search terms × multiple locations in one run (searchTerms, locations), with automatic cross-product and dedup.
Website platform detection (websitePlatform): WordPress, Wix, Squarespace, Webflow, Shopify, GoDaddy, Weebly, Jimdo, Duda, Drupal, Joomla, and more.
JSON-LD structured-data extraction — emails, phones, and social profiles from schema.org markup.
WhatsApp link extraction alongside existing social networks.
Website phone extraction (websitePhone) from JSON-LD and tel: links.
Contactability and opportunity subscores (contactabilityScore, opportunityScore) alongside the blended leadScore.
Structured address parts: street, city, postalCode, country.
language input (hl parameter + Accept-Language header).
Booking detection for 35+ platforms (Calendly, Fresha, OpenTable, Zocdoc, Doctolib, Booksy, Mindbody, ...) including iframe embeds.
Dataset views (Overview / Contact data / Opportunities) in the Apify Console.
RUN_SUMMARY key-value record with full run statistics and billable event counts.
Live status messages with progress during the run.
Impressum / legal-page crawling (often the only page with an email on EU sites).
Pay-per-event support with graceful stop when the charge limit is reached.
⚡ Lite mode (skipPlaceDetailPages): builds leads directly from search-result cards — ~100× fewer Google requests, designed for proxy-less runs. Website enrichment and scoring fully apply.
No-proxy resilience: exponential cooldowns (15s→120s) when Google rate-limits instead of hard failures, and automatic lead salvage — if a place page keeps failing, the lead is delivered from its search-result card data (leadsSalvagedFromFeed in RUN_SUMMARY).
Search-result card data (rating, review count, phone, website, address) is merged into place-page leads as a fallback — fixes missing review counts when Google serves its "limited view" to headless browsers.
Sponsored-result website links (google.com/aclk redirects) are unwrapped to the real site URL via adurl.
Fixed
bookingLinkFound false positives: the keyword book used to match every facebook.com link. Booking detection now uses word boundaries, platform allowlists, and social-host exclusion.
Localized label leakage: outputs like "Teléfono: +371 ..." are now clean values regardless of Google's language.
placeId was wrong: it contained the hex feature-ID; the real ChIJ... place ID is now extracted, and the decimal cid is correctly derived from the feature ID.
Missing coordinates: place URLs embed positions as !3d/!4d (not @lat,lng); both are now parsed, with a metadata fallback.
City parsed as postal code ("city": "LV-1039"): addresses are now parsed into street/city/postal/country with postal-code awareness.
Consent page on every navigation: a pre-accepted consent cookie is injected, with the dialog-clicking fallback retained. This also stops Google switching result language mid-run.
speedMode had no effect and crawler maxConcurrency was undefined; both are now wired into concurrency, delays, and timeouts.
History store wrote to the key-value store on every lead; writes are now batched and flushed on platform persist/migrate events.
onlyNewLeads semantics: a lead is "new" until it is actually delivered in a dataset, so leads dropped by other filters are not burned.
Blocked pages now retire the browser session and retry with a fresh fingerprint instead of being silently skipped.
Changed
Place pages are extracted in a single in-page pass (was 15+ sequential selector calls with multi-second timeouts) — typically 2-4× faster per place.
Images, media, and fonts are blocked during crawling — major bandwidth savings and faster navigations.
Proxy-free by design: the proxy configuration input was removed entirely. Lite mode is the default; detail-page mode relies on automatic cooldowns and lead salvage instead of proxies.
When narrowing filters are active, the Actor over-collects places (up to 2×) so runs still reach maxResults.
The crawl stops gracefully the moment maxResults leads are saved.
Default maxResults is now 50 for fast first runs (raise it for production runs).
Output keys are ordered for clean CSV exports (identity → score → contacts → location → metadata).
1.0 (2026-04-25)
Initial release: Google Maps search scraping, website enrichment, email/social/booking extraction, lead scoring, dedup, and new-lead tracking.