04 / Technical product

Turning a five-query research run into 297 ranked, structured business records — automatically

Manual local-market research is repetitive in a way that doesn't reward care: search, open each listing, check the website, copy fields into a spreadsheet, notice duplicates two hundred rows later. I built a modular Python engine that does that acquisition and enrichment work end to end — collecting public local-business listings, enriching each one from its own website, deduplicating, and scoring the result so a reviewer starts with the businesses worth calling, not an alphabetical dump.

Working technical demonstration · Quality hardening
StatusWorking technical demonstration · Quality hardening
TeamSolo engineering project
DemoControlled demo planned
SourcePrivate repository
Local Market Intelligence & Prospect Discovery Engine primary evidence
Context

Why the project mattered

Built for prospect-research workflows: a team needs to know which local businesses are active, whether they have a real web presence, and which ones are worth prioritizing — with a GUI for a one-off research pass and a CLI for repeatable, automatable jobs. It's the acquisition-mechanics counterpart to the other two data platforms in this portfolio: where the ATS Intelligence Engine solves normalization across ten APIs and Signal Intelligence solves evidence lineage across many source families, this project is about the raw mechanics of collecting and enriching public listing data reliably at volume.

My role and ownership

  • System architecture, GUI, CLI and checkpointing
  • The concurrent acquisition layer
  • The sitemap/BFS enrichment crawler that walks each company's own website
  • Normalization, deduplication and scoring logic
  • Multi-format export: CSV, JSON, styled XLSX

Personal contribution

Full solo ownership of the architecture, acquisition, enrichment, scoring and export pipeline.

Problem

The operational pain

Listing pages expose data in inconsistent, semi-structured form. Company websites hide contact information in different places — footer, contact page, a link buried three clicks deep in a menu. Repeated searches produce duplicate entries that quietly inflate a dataset. And a raw list of business names tells you nothing about which ones are actually reachable or worth a call, which is the only question that matters commercially once the research is done.

Before

  • Repeated manual searches and profile opening
  • Inconsistent contact, location and reputation fields
  • No systematic enrichment from company websites
  • Duplicate records across related queries
  • No transparent way to prioritize prospects

After

  • Concurrent acquisition with isolated worker contexts
  • Normalized 38-field business records
  • Bounded website crawl using sitemaps, JSON-LD and contact-page prioritization
  • Deterministic deduplication and streaming export
  • Separate data, contact and outreach-priority scores
Solution

How the system works

Acquisition runs on Playwright/Patchright for dynamic listing pages, orchestrated asynchronously with isolated browser contexts so multiple queries run concurrently without interfering with each other. A separate aiohttp crawler handles each business's own website — faster than a full browser when static HTML is enough to find a footer email address, falling back to fuller rendering when it isn't. Every record normalizes into a 38-field schema, gets deduplicated against everything already collected in the run, and is scored into a tier based on review count, rating and phone/website presence — so the output a reviewer opens is a prioritized shortlist, not a flat alphabetical table.

Records stream to CSV as they're collected rather than buffering in memory until the run finishes, with JSON and styled XLSX export layered on top for whichever format the next step in the workflow needs. A selector-health monitor compares field-fill rates run over run, so if a listing site changes its markup, that shows up as a measurable, visible drop in coverage instead of silently corrupting the dataset with empty or malformed fields nobody notices until a client does.

01QueriesBusiness + city intent
02AcquisitionRendered public listings
03EnrichmentWebsites and JSON-LD
04NormalizePhones and deduplication
05PrioritizeScores and exports
Architecture workflow
Engineering

Architecture and decisions

Acquisition and intelligence are deliberately separate layers: extraction gathers evidence, and scoring/classification live in a dedicated module downstream — which means the business logic that decides "is this prospect worth a call" is inspectable and testable on its own, not buried inside scraping code. Every long research run checkpoints and streams results incrementally, so an interruption costs minutes of re-work, not the whole run.

DECISION 01

Separate acquisition from intelligence

Extraction gathers evidence while scoring and classification live in a dedicated layer, making the business logic inspectable and independently testable.

DECISION 02

Bound website enrichment

Sitemap discovery and contact-page relevance improve coverage, while page caps and early stopping prevent wasteful, unbounded full-site crawling.

DECISION 03

Stream results and preserve progress

Incremental writes and checkpoints reduce the cost of interruptions and make long research runs safe to leave running unattended.

DECISION 04

Expose both a GUI and a CLI

The GUI supports a human operator running a one-off pass; the CLI supports repeatable jobs, scheduling and technical inspection of exactly what ran.

Visual proof

Evidence that can be inspected.

A supplied 297-record Casablanca demonstration run across five queries, shown exactly as it was captured.

Outcome

Value and honest current state

Operational impact

The supplied Casablanca run turned five search queries into 297 unique, normalized business records automatically: 95.6% with a working phone number, 98.0% with coordinates, 49.2% with a website, 14.1% with an email — the kind of coverage that turns a research task measured in days into one measured in minutes.

Verified evidence

297 unique records in the supplied demonstration run
38 normalized fields per record
95.6% phone coverage, 98.0% coordinate coverage in the demo
CSV / JSON / XLSX export and checkpoint/resume implemented, not aspirational

Current limitations

The 297-record run is one supplied demonstration, not a claim about performance at every scale or category. Auditing that dataset surfaced three real data-quality issues worth fixing before the next run: 137 rows carry an incorrect category from a selector fallback, 289 descriptions duplicate the address field, and roughly 188 secondary-phone values just repeat the primary number. None of that is hidden — the selector-health monitor exists precisely to catch this class of problem, and these three are its first real findings.

Project status: Working technical demonstration · Quality hardening. A controlled demo is planned once the flagged data-quality issues are resolved.

Need structured market data, not a raw export?

Acquisition, enrichment, deduplication and scoring, built for a workflow that needs the result prioritized, not just collected.

Discuss a project ↗