← back to Lawyer Directory Builder
README.md
98 lines
# LA Lawyer Directory — Office Map
Compliance-first PostgreSQL database of **law firm offices in LA County**, focused on
geographic clustering (Beverly Hills, Century City, DTLA, etc.). Powers profile-site
generation, SEO landing pages, directory pages, and CRM enrichment.
## Status
| | |
|---|---|
| DB | `lawyer_professional_directory` on `127.0.0.1:5432` (Unix socket) |
| API | `http://localhost:9701` |
| Dashboard | `http://localhost:9701/` (Leaflet map of every geocoded firm) |
Sources currently wired (free, no auth, robots-clean):
- **OpenStreetMap Overpass API** — every `office=lawyer` / `amenity=lawyer` node in LA County.
- **Wikidata SPARQL** — notable firms with HQ/office in LA County (Latham, Gibson Dunn, Paul Hastings, Manatt, Lewis Brisbois, Littler Mendelson, Irell, etc.).
Sources blocked by anti-bot — see `docs/blocked_sources.md`:
- CA State Bar attorney search (SPA, JS-only render)
- CA SOS bizfile API (recaptcha-gated)
Both are reachable with a Playwright pass — that's the next milestone, not tonight's.
## Run
```bash
npm install # one-time
npm run migrate # create / update tables (idempotent)
npx tsx src/server/index.ts & # start API + dashboard on :9701
npx tsx src/scripts/run_overnight.ts # one-shot import pass + summary
```
Logs land in `logs/overnight-YYYY-MM-DD.log`.
## Schedule it nightly
Pick one:
**cron** (macOS):
```cron
# nightly at 02:00 local
0 2 * * * cd /Users/stevestudio2/Projects/lawyer-directory-builder && /usr/local/bin/npx tsx src/scripts/run_overnight.ts >> logs/cron.log 2>&1
```
**pm2** (if installed):
```bash
pm2 start npx --name lawyer-server -- tsx src/server/index.ts
pm2 start --cron-restart "0 2 * * *" --no-autorestart \
npx --name lawyer-overnight -- tsx src/scripts/run_overnight.ts
pm2 save
```
## Useful queries
```sql
-- top neighborhoods by firm count
SELECT COALESCE(neighborhood, city) AS area, COUNT(*) AS firms
FROM organizations WHERE type='law_firm'
GROUP BY area ORDER BY firms DESC LIMIT 20;
-- multi-firm buildings (true clusters)
SELECT address, city, COUNT(*) AS firms,
ARRAY_AGG(name ORDER BY name) AS firms_at_address
FROM organizations
WHERE type='law_firm' AND address IS NOT NULL
GROUP BY address, city HAVING COUNT(*) > 1
ORDER BY firms DESC;
-- 90067 Century City roster
SELECT name, address, phone, website
FROM organizations
WHERE type='law_firm' AND zip='90067'
ORDER BY name;
```
## API endpoints
| | |
|---|---|
| `GET /api/health` | liveness |
| `GET /api/stats` | totals + coverage |
| `GET /api/firms?city=Beverly+Hills&limit=200` | firms list |
| `GET /api/firms/:id` | one firm |
| `GET /api/cities` | firm count + centroid by area |
| `GET /api/buildings` | multi-firm addresses |
| `GET /api/heatmap` | `{lat,lng,weight}` for map overlay |
| `GET /api/jobs` | last 30 scrape_jobs runs |
## Schema
Mirrored on the `organizations` + `professionals` + `professional_locations`
join model. See `migrations/001_initial_schema.sql` (base) and
`migrations/002_firm_office_columns.sql` (clustering: `neighborhood`, `lat`,
`lng`, `address_norm`, `attorney_count`, `firm_size_band`).
Every fact has a `source_url` traceable in `raw_records`.