← back to Nationalrealestate
src/ingest/listings/types.ts
53 lines
/**
* M-B3 firm↔listing pilot types.
*
* HARD LEGAL RAIL: a ListingFacts row carries FACTS ONLY. No photo URLs, no agent
* remarks, no marketing description — those are copyrighted and must never be stored.
* Every row keeps `url` (the broker's own listing page) so we link out, never present
* a listing as ours.
*/
export interface ListingFacts {
sourceId: string; // stable per-listing id from the source (URL token / MLS#)
url: string; // canonical listing URL on the broker's own site
address: string | null;
city: string | null;
state: string | null; // 2-letter, used for firm + region crosswalk
zip?: string | null; // postal code — ZIP→county region fallback when lat/lng absent
price: number | null;
beds: number | null;
baths: number | null;
sqft: number | null;
lat: number | null;
lng: number | null;
status: string | null; // 'active' | 'sold' | 'pending' | null
}
export interface ListingAdapter {
/** stored in listing.source and ingest_runs.source */
source: string;
/**
* INGEST MODE — declares whether a run of this adapter FULLY re-scans the source's live
* listing set ('full') or only pulls newly-added listings ('incremental', e.g. a new-day
* sitemap that never re-surfaces old-but-still-active listings).
*
* This is load-bearing for withdrawal detection (TK-10155 fix 1): a listing that goes
* "gone from feed" can only be treated as stale/withdrawn on a source that has had a FULL
* rescan AFTER the listing's last_seen. Incremental-only sources structurally cannot prove
* absence, so their listings must NOT be aged into withdrawn. Recorded per run into
* source_ingest_run (migration 015) and read back by classifyState.
*/
mode: 'full' | 'incremental';
/** host used to resolve firm_id via firm_site.url domain match */
host: string;
/** robots.txt path the adapter reads listings under — used for the honor check */
listingsPathHint: string;
/**
* Yield up to `cap` listing PDP URLs from the site's public sitemap/index.
* Each URL must be robots-allowed (the engine re-checks before fetch).
*/
discover(cap: number): Promise<string[]>;
/** Fetch one listing URL and extract FACTS ONLY (JSON-LD). null = unparseable, skip. */
parse(url: string, html: string): ListingFacts | null;
}