Back to Home

clientscout

A repeatable Python pipeline for local business prospect research.

the problem and my contribution

Repeated Maps searches produce inconsistent fields and duplicate businesses. I built a Python pipeline that turns those records into a prioritized prospect CSV with stable IDs and campaign history.

My work covers orchestration, normalization, deduplication, activity checks, and exports using Python's standard library. Collection uses a third-party Docker scraper; processing is rule-based.

prospect research pipeline

01 · OwnerDefine campaignLocations + categoriesGenerate search queries02 · ScraperCollect businessesGoogle Maps via DockerPreserve raw records03 · CodeNormalize fieldsNames, contacts, URLsOne consistent schema04 · CodeRemove duplicatesPlace, domain, phoneThen name + city05 · CodeClassify websitesNone, social, free builderExclude custom sites06 · CodeCheck activityExclude closed / staleKeep uncertain records07 · CodeSync historyStable IDs across runsRefresh master records08 · CodePrioritize & exportWebsite type + activityClean prospect CSV09 · OwnerReview prospectsVerify business detailsChoose whom to contact

Raw records stay intact. Filtered businesses remain in the full dataset for traceability.

recorded campaign results

Saved campaign outputs · September 2026
CampaignRaw recordsDeduplicatedProspects
Fredericksburg / StaffordSep 3 · 8 queries18117031
Virginia dental / aestheticsSep 9 · 128 queries1,9001,47159
Virginia contractors / home servicesSep 9 · 256 queries4,6154,263362

Row counts from each campaign's raw.csv, all-businesses.csv, and prospects.csv. Prospects are exported records for review, not sales outcomes; campaigns may overlap.

engineering decisions

In-process orchestration
Resolve configuration once and pass it through all three stages. Campaign identity and settings stay consistent.
src/build.py · excerpt; logging omitted
cfg = get_runtime_config(sys.argv[1:])

queries.run(cfg)
scrape.run(cfg)
process.run(cfg)
Identity-aware deduplication
Match by place ID, custom domain, phone, then name and city. Restrict domain matching to custom sites so shared social platforms do not merge unrelated businesses.
src/process.py · domain-match excerpt
dom = domain_key(lead["website"])
if dom and lead["website_type"] == "custom" and dom in domain_idx:
    merge_into(domain_idx[dom], lead)
    continue
Bounded failure handling
Review lookups use timeouts, limited concurrency, and cached results; failures retain an unverified status. Scraper shutdown is bounded, and processing requires output from the current run.

repeatable runs

CLI · example campaign
python src/build.py --campaign plumbers-fxbg \
  --location "Fredericksburg VA" --category "plumber"

python src/process.py --campaign plumbers-fxbg

Campaign folders protect existing output. Reprocess saved records without scraping again; stable IDs support repeat imports into outreach tools.

source limits

Maps links and review dates are signals, not proof of website need or business closure. The undocumented review endpoint can fail; uncertain records stay eligible for human review. Outreach happens outside ClientScout.