Find businesses via Google Maps, enrich via Google Search. Uses Serper.dev to sweep cities with geo-grids, then runs customizable search queries to find owners, LinkedIn profiles, or any data you need. Apply quality filters and export clean, deduplicated leads ready for outreach.
Two-phase discovery engine: Phase 1 uses pagination (up to 30 pages) for broad coverage, Phase 2 fills gaps with targeted geo-grid search (max 3 pages/cell)
Smart grid skip: If pagination finds zero results for a keyword+city, the grid phase is skipped entirely to save API credits
Relevance filtering (isRelevantPlace): Filters out irrelevant place types (atm, bus_station, parking, etc.) — applied always in grid phase, after page 10 in pagination
Dynamic zoom calculation: Grid zoom level is now computed from stepKm (round(14 - log2(stepKm))) instead of hardcoded 15z
includeUnratedBusinesses option: New input toggle to include businesses with no rating/reviews even when minRating or minReviews filters are active (default: true)
SSRF prevention in website scraper: Blocks requests to internal/private IPs, localhost, metadata endpoints, and IPv6 local ranges
Social URL validation: Filters out share/intent/dialog URLs before extracting social profiles
Social URL canonicalization: Normalizes LinkedIn, Facebook, Instagram, Twitter, and TikTok URLs to consistent canonical format
Separate LinkedIn fields: Scraper now returns linkedinCompany and linkedinProfile separately, with company page preferred over personal profile
Impressum owner extraction: Detects owner/founder names from German Impressum and English About pages via regex patterns
City match scoring (+8 points) in enrichment when city name appears in LinkedIn snippet
Phone input sanitization: Handles multi-number fields (splits on /;,|), strips tel:/phone: prefixes before normalization
Country code normalization: Maps common variants (UK→GB, USA→US, etc.) for reliable phone number parsing
Coverage flags on output: hasWebsite, hasPhone, hasLinkedIn, hasOwnerName boolean fields for quick filtering
domainRoot field restored on enriched output for domain-level deduplication
enrichmentScore and enrichmentStrong fields on output for enrichment quality assessment
Changed
Geo-grid distance calculation upgraded from flat-earth Euclidean to Haversine formula for accuracy at larger radii
Geo-grid edge handling: Added clampLatitude() (±90°) and wrapLongitude() (±180°) for antimeridian and pole safety
Serper error handling redesigned with four error categories: auth (401/403, no retry), quota (429, 3^attempt backoff), transient (5xx/network, standard backoff), permanent (4xx, no retry)
Search templates updated to use intitle: operators for more precise LinkedIn title matching with negative filters (-intitle:"Assistant", -intitle:"Intern")
Enrichment scoring now uses separate thresholds: MIN_PROFILE_SCORE=50 and MIN_COMPANY_SCORE=30 (previously single threshold)
Scraper client rotation: Impit client is now recycled every 500 requests with mutex lock to prevent concurrent client creation
Nominatim geocoder now uses AbortController with 10s timeout to prevent hanging requests
HTML entity decoding added to search template processing (handles ", &, etc. from frontend input)
Search query intent sorting: Queries are sorted by priority (website_contact > linkedin_company > linkedin_profile > general) for optimal API credit usage
ReDoS protection: Impressum regex patterns are now limited to 5000 character input to prevent catastrophic backtracking
Fixed
LinkedIn enrichment now correctly distinguishes between company pages (/company/) and personal profiles (/in/) instead of treating all as generic LinkedIn
Geo-grid cells near poles no longer cause division-by-zero errors (longitude step clamped to minimum 0.1)
Auth errors (401/403) no longer trigger infinite retry loops
Scraper client memory leak fixed with proper close()/destroy() cleanup on rotation
Phone numbers with multiple values separated by / or ; are now correctly split and normalized individually
[8.8.x]
Added
Automatic pagination: fetches up to 5 pages per search location (100 businesses per grid cell)
Dual output modes: "Maps Only" for raw data, "Full Enrichment" for phones, socials, and owner info
Technology badges in README (Serper, Nominatim, Cheerio, libphonenumber)
Detailed configuration guide for radiusKm and stepKm with presets and examples
TikTok and Instagram profile detection from business websites
Nominatim geocoding cache using Apify KeyValueStore (respects usage policy)
Serper API retry logic with exponential backoff for 5xx, 429, and network errors
API key format validation (32-64 hex characters required)
State persistence / checkpointing saves progress every 100 leads for crash recovery
Graceful shutdown handlers for SIGTERM and SIGINT signals
Global error boundaries for unhandledRejection and uncaughtException
Input sanitization function to prevent XSS and injection attacks
Increased limits: perCityTarget max 10,000, searchNum max 100, maxSearchQueriesTotal max 1,000,000
Raw Search Results Mode (Developer Mode): New rawSearchResults option that returns Maps data + raw search results without processing; ideal for LLM analysis and custom enrichment pipelines
Changed
Switched to /maps endpoint for better results (20 per request, includes placeId and fid)
Cleaner log output: removed verbose API call logs and website scraping messages
Config summary now displays as aligned bullet points instead of one long line
Time format simplified (removed "Coordinated Universal Time")
Phone numbers are validated before output to filter corrupted data
Dockerfile optimized with multi-stage build (smaller production image)
TypeScript strict mode enabled with noUncheckedIndexedAccess and other strict checks
Scraper client now uses singleton pattern to prevent memory leaks
HTTP client now properly cleaned up on exit in all cases
Fixed
Corrupted phone numbers from Serper (e.g., "4,7(3549)") are now filtered out
placeId uses cid as fallback when not provided by API
Deduplication now uses placeId as primary key for reliability
Empty error handlers now properly log failures instead of silently failing
Memory leak in website scraper fixed with singleton Impit client
TypeScript strict mode errors all resolved (18 errors fixed)
Removed
city field from output (use address instead)
domainRoot field from output
Verbose "Scraping: URL" and "Found LinkedIn match" log messages
[8.7.0]
Added
Nominatim integration for converting city names to coordinates
Geo-grid builder for comprehensive area coverage using radius and step size