← back to Re Flyers
PLAN.md
63 lines
# PLAN — normalized cross-source flyer index (TK-10708)
## Position (why this exists next to re-flyer-aggregator)
`re-flyer-aggregator` already DISCOVERS + GENERATES flyers, but its outputs live in three
different shapes (a deal-scoped PG view `deal_assets_v`, a broker-scoped `property-flyers.json`,
and loose `out/spotlight-*.html` files). There is no single normalized, deduped index in the
schema TK-10708 asks for. `re-flyers/` is that thin **unifier** — it READS the existing outputs
(no re-scrape, $0 local) and normalizes them into one feed + a sort/density viewer. It does not
duplicate the discovery/generation engine.
## Aggregation architecture (5 bullets)
1. **Collect (read-only) from 3 existing producers** — Source A `property-flyers.json` (broker
OMs/brochures, Tier-1), Source B reflyers PG `external_marketing_asset` (classified deal
assets), Source C `out/spotlight-*.html` (Tier-2 self-generated recaps). `scripts/build-seed.mjs`
is the collector; it re-runs any time to refresh.
2. **Normalize to one schema** —
`{ source_build, listing_id, title, address, flyer_url, generated_at, asset_type,
rights_basis, tier, dedup_key }`. `source_build ∈ {broker-site, usre-deal, rentv-spotlight}`
maps each row to its producing RE surface; `asset_type` uses the aggregator's controlled
vocabulary (`offering_memorandum`, `marketing_flyer`, `property_spotlight`, `property_brief`,
`market_report`, `deal_recap`).
3. **Dedup strategy** — `dedup_key = sha1(lower(flyer_url || title))[:12]`. Same PDF/landing URL
discovered under two sources collapses to one row (first writer wins). URL is the strong key;
title is the fallback for self-generated recaps that have no external URL. Future upgrade:
dedup by subject (`county_fips + ain` / `doc_number`) once broker rows carry a resolved parcel.
4. **Compliance carried through, never bypassed** — every row keeps `rights_basis` + `tier`. The
viewer flags GATED/Tier-3 rows (dashed, 🔒, link suppressed) so they're visible but not
clickable-to-fetch. The index references URLs only — it **never** downloads or re-hosts a file.
Promoting anything into a live consumer (usre/CRCP/RENTV) stays GATED to Steve.
5. **Serve + view** — `server.js` exposes `GET /api/flyers` (serves `data/flyers.json`) behind
basic-auth `admin/DW2024!` on an OS-assigned free port (no permanent port bound).
`public/index.html` is the grid with the mandatory **sort** (Newest / Source / Type / Tier /
Title A→Z) + **density slider** (2–7 cols) + source filter + search, all localStorage-persisted
per Steve's standing grid rule.
## Schema (the normalized row)
| field | source | notes |
|---|---|---|
| `source_build` | derived | `broker-site` \| `usre-deal` \| `rentv-spotlight` |
| `listing_id` | source | deal doc-id for recaps; null for broker/deal rows until parcel-resolved |
| `title` | source | property/brochure name or recap `<title>` |
| `address` | source | null today (broker flyers carry a name; upgrade = parcel join) |
| `flyer_url` | source | PDF/landing URL, or `file://` for local Tier-2 recap |
| `generated_at` | source | `found_at` / `last_verified_at` / file mtime |
| `asset_type` | source | controlled vocab (above) |
| `rights_basis` | source | `first_party_broker` \| `self_generated` \| `GATED_needs_review` \| … |
| `tier` | source | 1 \| 2 \| 3 |
| `dedup_key` | derived | sha1 collapse key |
## Roadmap
1. **Done (this scaffold):** collector, normalized `data/flyers.json` (224 rows), `/api/flyers`,
sort+density viewer, RESEARCH/PLAN.
2. **Next (safe, local):** resolve `address` + `listing_id` by joining broker flyers to usre
parcels (subject-level dedup); a scheduled `build-seed` refresh; pull in the aggregator's
`deal_assets_v` directly from usre for the richer deal join.
3. **Gated (needs Steve):** wire this feed into CRCP/RENTV/usre as a live consumer surface; any
Kamatera deploy; promote validated rows into usre; any Tier-3 marketplace access or PDF re-host;
any send-to-list. None of these are executed here.