← back to Ventura Corridor
README.md
93 lines
# Ventura Corridor
Local-only directory of every business on Ventura Blvd (Calabasas → North Hollywood). Inventory + front-page audit + SEO scoring + ownership classification + three view surfaces.
**Loopback only** — bound to `127.0.0.1:9780`, never exposed to LAN/tailnet/internet.
## Surfaces
| URL | Purpose |
|---|---|
| `http://127.0.0.1:9780/` | 2D Leaflet map · noir-themed · grid-search · live build-status strip |
| `http://127.0.0.1:9780/3d.html` | Three.js "signal corridor" · screenshot billboards on real Ventura Blvd polyline · drive-cam |
| `http://127.0.0.1:9780/wall.html` | Pinterest-grid of every captured front-page screenshot |
## Pipeline
```
ingest (OSM Overpass, free, no key) → 224 businesses
↓
crawl (Playwright, conc 20) → 113 front-page audits + screenshots
↓
score (12-signal SEO model) → tier A/B/C/D + per-category rank
↓
enrich:classify (heuristic ownership) → 180 indep / 23 corp / 20 franchise
↓
viewers (2D / 3D / wall)
```
## Layout
```
~/Projects/ventura-corridor
├── src/
│ ├── server/index.ts # Express on 127.0.0.1:9780
│ ├── ingest/
│ │ ├── osm_overpass.ts # primary (free, no key)
│ │ ├── here_discover.ts # secondary (HERE_API_KEY required)
│ │ └── foursquare.ts # secondary (FOURSQUARE_API_KEY required)
│ ├── crawl/front_page.ts # Playwright crawler
│ ├── score/seo_score.ts # 12-signal scorer
│ └── enrich/
│ ├── classify_ownership.ts
│ └── restaurant_owners.ts # DDG-search; rate-limited, low yield
├── public/ # 2D, 3D, wall
├── data/
│ ├── ventura-blvd-polyline.json # 214-pt corridor centerline
│ ├── screenshots/ # firm-NNN.jpg × 113
│ ├── raw/ # firm-NNN.html × 113
│ └── last-verified.json
├── db/migrations/ # 001_init.sql · 002_enrichment.sql
├── ecosystem.config.cjs # pm2
└── .env # local config
```
## Run
```bash
npm install
npm run migrate # apply schema
npm run ingest:overpass # pull businesses
npm run crawl # capture front-page audits
npm run score # compute SEO score per business
npm run enrich:classify # tag ownership class
pm2 start ecosystem.config.cjs # start the read API + viewers
pm2 save
```
Health: `curl http://127.0.0.1:9780/api/health`
## Schema
- `businesses` (anchor) — 1 row per business · `(source, source_id)` UNIQUE
- `corridor_segments` — 26 polyline-segment rows for the boulevard
- `front_page_audits` — 1 row per crawl pass (HTTPS, viewport, schema.org count, alt-coverage, etc.)
- `seo_scores` — 1 row per scoring pass (12 weighted signals → total / tier / category_rank)
- `business_enrichment` — 1 row per business · ownership_class · parent_brand · best_contact_path
- `business_contacts` — many per business · LinkedIn / website-about / GMB sources
- `ingest_runs` — provenance log
## Standing rules in this codebase
- **Local-only.** Never bind to `0.0.0.0`. Never deploy to Kamatera without explicit deploy-intent confirmation.
- **No fabricated stats next to real entity names.** Same compliance discipline as the lawyer/doctor projects.
- **No customer-routing language anywhere.** This is a directory, not a referral service.
- **Grid-search is mandatory** on every surface (per MEMORY.md `feedback_every_site_grid_search.md`).
- **Color-dot indicator pattern is DW-micro-site only.** Not used here.
## Known gaps
- OSM coverage west of Tarzana is sparse (1 row in Woodland Hills, 0 in Calabasas/Tarzana). Layer HERE or Foursquare ingest when the keys are available.
- 26 sites failed the front-page crawl (DNS errors on chain-locator URLs, anti-bot walls). Re-crawl with longer timeouts is wired (`npm run crawl -- --rescore`).
- LinkedIn enrichment via DDG hit DDG's rate limit at request 40 of 89; only 1 contact captured. Apollo.io free tier (50 credits) would close this if Steve wants.