- types.ts: GazetteerEntry / SearchQuery / SearchHit / SearchSource (D-03/D-07/D-08) — byId() is separate and synchronous; empty search() array = honest refusal - rules/abbreviations.ts: the vendored JUPEM table (49 rows incl. Presint→Prsn vowel-drop and MS 2039 A3 Kpg forms) as a ReadonlyArray of pairs, inverted once by a single generic builder — extends by file edit, never a code change (D-09) - normalize.ts: NFKD fold → punctuation collapse (keeps '/') → whole-token expansion (canonical = long form) → longest-first glued split (remainder >= 3) T1-T9 green; typecheck clean.
64 lines
2.4 KiB
TypeScript
64 lines
2.4 KiB
TypeScript
/*
|
|
* types.ts — the search contracts (D-03, D-07, D-08, D-11).
|
|
* --------------------------------------------
|
|
* GazetteerEntry / SearchQuery / SearchHit / SearchSource are the seam
|
|
* every backend implements: the in-Worker gazetteer (backend #1, this
|
|
* phase), the Malaysia-only WASM geocoder (backend #2, D-05, post-v1),
|
|
* and Photon-on-VPS (backend #3, D-05, Phase 5). Backend #2/#3 do NOT
|
|
* ship in this phase — the interface exists so they can be added without
|
|
* a rewrite.
|
|
*
|
|
* SearchSource.search() returns an EMPTY array as a legitimate,
|
|
* load-bearing value: "no match in the KL gazetteer" (D-13's
|
|
* out-of-area negatives must be refused honestly, never answered with
|
|
* the nearest KL pin).
|
|
*
|
|
* byId() is separate and SYNCHRONOUS: the alternates callback handler
|
|
* and the webview's deep-link reader both need an id -> entry lookup and
|
|
* neither should await a network.
|
|
*
|
|
* Type-only module: no runtime values, no imports, no storage (GEO-03).
|
|
*/
|
|
|
|
/** One curated gazetteer landmark (D-07 typed JSON, D-08 native aliases). */
|
|
export interface GazetteerEntry {
|
|
/** Stable, slug-shaped: "suria-klcc", "sunway-pyramid". Never renamed once shipped. */
|
|
id: string;
|
|
/** Canonical display name (the OSM landmark name, e.g. "Suria KLCC"). */
|
|
name: string;
|
|
/** Locals' shorthand (D-08) — never empty; always includes the canonical name. */
|
|
aliases: string[];
|
|
lat: number;
|
|
lng: number;
|
|
/** "mall" | "office-tower" | "park" | "station" | ... (small closed set). */
|
|
category: string;
|
|
/** Human address for sendVenue's `address` field — shown VERBATIM, never normalized. */
|
|
address?: string;
|
|
}
|
|
|
|
/** A search request. */
|
|
export interface SearchQuery {
|
|
/** Raw user query. */
|
|
text: string;
|
|
/** Default 5 (1 best + 2-4 alternates, D-02). */
|
|
limit?: number;
|
|
}
|
|
|
|
/** One ranked result. */
|
|
export interface SearchHit {
|
|
entry: GazetteerEntry;
|
|
/** 0..1, higher is better. */
|
|
score: number;
|
|
/** Which alias matched — the corpus report needs this provenance. */
|
|
matchedAlias: string;
|
|
}
|
|
|
|
/** The pluggable search seam (D-03). Backends: "gazetteer" | "wasm-geocoder" | "photon". */
|
|
export interface SearchSource {
|
|
readonly name: string;
|
|
/** EMPTY array = honest refusal (D-13) — never a nearest-match pin. */
|
|
search(q: SearchQuery): Promise<SearchHit[]>;
|
|
/** Synchronous id -> entry, honest null when unknown. */
|
|
byId(id: string): GazetteerEntry | null;
|
|
}
|