/** * Server-side keyword extraction from listing descriptions — the * canonical helper reconciling five cohort implementations. * * Each cohort MCP ships a byte-for-byte `src/features.ts` (round-4 * candidate J): the same `ExtractedFeatures` shape, the same * pre-compiled regexes, the same detector ordering. Real-world callers * (zillow-mcp #14: a 53-listing session, ~100 KB of marketing copy) * immediately keyword-parse the prose for the same handful of features * — lake/waterfront, hot tub, basement state, furnished, dock, * community — and discard the rest. This module lifts that work so * each consumer can drop the raw description by default. * * Surveyed implementations (`src/features.ts` in each): * * - onehome-mcp — CANONICAL. Its basement detector uses a * `BASEMENT_CONNECTOR` conjunct class `(?:is|was|are|were|—|–|,|;|:|()` * rather than the looser `[^.!?]{0,30}?` window the others use. * The tight class rejects free-floating prepositions like "with", * so "basement with finished oak shelving" no longer false-positives * to `finished` (the shelving is finished, not the basement). * - zillow-mcp / redfin-mcp / compass-mcp / homes-mcp — identical * extraction logic; only the looser basement window and the * `loadCommunities` caching variants differ. * * Pure / dependency-free by design. `extractFeatures` takes a * description string AND a community-name vocabulary (a `string[]`, * possibly empty) — the consumer resolves that vocabulary separately. * The cohort's `loadCommunities` does filesystem I/O (reads a JSON * file named by an env var) and therefore stays per-consumer: pulling * it here would add a `node:fs` dependency and break realty-core's * no-I/O invariant. This module only consumes the resolved list. * * The detector to remember: `unfinished basement` is checked BEFORE * `finished basement` because `finished` substring-matches inside * `unfinished`. That ordering is the regression-pinned subtle-bug guard. */ /** Structured features extracted from a listing description. */ export interface ExtractedFeatures { /** True when the prose mentions lakefront / lake front / waterfront. */ lake_front: boolean; /** True when the prose mentions a hot tub. */ hot_tub: boolean; /** * Basement state. `'unknown'` means a basement is mentioned without a * recognized finish state; `null` means no basement mention at all. */ basement: 'finished' | 'unfinished' | 'partial' | 'unknown' | null; /** Furnishing level, or `null` when not mentioned. */ furnished: 'fully' | 'partial' | 'negotiable' | null; /** Dock / water-access type, or `null` when not mentioned. */ dock: 'private' | 'community' | 'marina' | 'boat_slip' | null; /** First-by-position matched community name, or `null`. */ community: string | null; } /** * Extract structured features from a listing description. * * Pure / deterministic — no I/O, no dependencies. Hoisted from the * realty cohort (zillow/redfin/compass/homes/onehome), with onehome's * tighter basement detector as the canonical form. * * @param description Listing prose; `undefined` is treated as empty. * @param communities Resolved community vocabulary (may be empty). The * earliest match by document position wins — see the community * detector. The consumer resolves this list (e.g. its own * `loadCommunities`); this function never reads files or env. * * @example extractFeatures('Lakefront with a private dock.', []).dock // 'private' * @example extractFeatures('Basement with finished oak shelving.', []).basement // 'unknown' */ export declare function extractFeatures(description: string | undefined, communities: string[]): ExtractedFeatures;