Skip to content

Repository files navigation

LeadScout

LeadScout discovers local businesses from Google Maps for a keyword + location search, extracts structured business details, classifies whether each business has a website, and lets you filter and export leads as CSV.

Phase 1's flagship use case: find businesses in a target market that do not have a website.

Keyword:  roofing contractor
Location: Texas, USA
Filter:   Businesses without websites

This is Phase 1 of a planned local-business intelligence / lead-generation platform — see docs/roadmap.md for what's deliberately not built yet (website analysis, AI lead scoring, CRM sync).

Architecture

Chrome Extension (MV3, React, TypeScript)
        │  REST API
        ▼
FastAPI Backend  ──►  Scraping Engine (Playwright)  ──►  SQLite  ──►  CSV
  • The extension is a thin REST client. All scraping happens server-side.
  • Jobs are asynchronous: the extension creates a job, gets a job_id immediately, and polls for status/results — it never blocks on a scrape.
  • Website presence is a tri-state (PRESENT / ABSENT / UNKNOWN), not a boolean — a failed extraction is never silently treated as "no website."

Full design: docs/architecture.md and docs/scraper-design.md. FOSS projects studied for architectural patterns (not copied — clean-room implementation): see docs/foss-references.md.

Features

  • Keyword + location search over Google Maps
  • Structured extraction: name, category, address, phone, website, rating, review count, coordinates, place identifier, business status
  • Tri-state website detection with a documented, conservative rule set
  • ALL / NO_WEBSITE filtering (architecture supports adding more filters without touching the UI — see app/services/filter_service.py)
  • Deterministic deduplication (place ID → canonical Maps URL → normalized name+address+phone), so re-running a search doesn't create duplicate leads
  • Bounded retries with exponential backoff + jitter, classified errors, configurable concurrency
  • CSV export with human-readable column headers
  • Structured (JSON line) logging of the scrape lifecycle

Project layout

extension/   Chrome extension (MV3, React, TypeScript, Vite)
backend/     FastAPI + Playwright backend
docs/        Architecture, scraper design, API reference, FOSS references, roadmap

Installation & local development

See docs/development.md for full setup (backend, Playwright browser install, extension build, load-unpacked instructions, testing, configuration).

Quick start:

# Backend
cd backend
uv venv && source .venv/bin/activate
uv pip install -e ".[dev]"
python -m playwright install chromium --with-deps
cp ../.env.example .env
uvicorn app.main:app --reload --port 8000

# Extension (separate terminal)
cd extension
npm install
npm run build
# chrome://extensions → Developer mode → Load unpacked → extension/dist

Testing

cd backend && pytest
cd extension && npm run lint

Unit tests never touch a browser or network. Integration tests exercise the FastAPI app with the scraper engine mocked out. See docs/development.md for the opt-in live Google Maps test.

Configuration

See .env.example for every backend environment variable (concurrency limits, retry/backoff, timeouts, database URL).

CSV export

GET /api/jobs/{job_id}/export.csv — see docs/api.md for the full API reference and column list.

Releases

backend/ and extension/ version independently via mise + semantic-release, driven by Conventional Commits. See docs/RELEASE.md for the two release streams, tag formats, and the release:drift / release:dry / release:full commands.

Known limitations

  • Google Maps markup is undocumented and can change. All selectors are centralized in backend/app/scraper/google_maps/selectors.py; when Maps changes, that's the one file that needs updating.
  • English (US) UI assumed. Text-based signals ("Permanently closed", address/phone aria-label prefixes) are matched against the English UI and will need adjustment for other Maps locales.
  • rating/review_count are best-effort. Google Maps hydrates its review summary slightly after the core panel fields; these are left null rather than guessed when not captured in time. website_status does not have this issue — it has a strict, documented tri-state rule set.
  • Narrow single-result searches (a keyword+location combination that matches essentially one business) render that business's detail panel directly instead of a results list. LeadScout handles this, but the Google-assigned place identifier and coordinates aren't recoverable from the URL in that case, so deduplication falls back to the name+address+phone composite key for that business.
  • No street/city/state/zip address parsing — only whitespace normalization is applied to addresses.
  • Single-machine, single-user Phase 1. Jobs run as in-process asyncio tasks (see docs/architecture.md §6 for the documented path to out-of-process workers); there is no authentication or multi-tenancy; backend CORS is permissive, appropriate for a local tool, not a hosted multi-user service.
  • No extension icon — Chrome shows its default icon for the unpacked extension.

Responsible use

LeadScout automates browsing publicly viewable Google Maps listings via a standard browser (Playwright/Chromium) at a configurable, bounded concurrency. It does not attempt to bypass CAPTCHAs, authentication, or any other access control, and it is not designed to. You are responsible for complying with applicable laws and the terms governing any service you use LeadScout against, and for the concurrency/rate settings you configure.

License

MIT — see LICENSE.

About

Google Maps lead discovery: find local businesses without a website. Chrome extension + FastAPI/Playwright backend.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages