/** * @copyright Sister Software * @license AGPL-3.0 * @author Teffen Ellis, et al. * * Soft-prior emission biases derived from the FST gazetteer. When the FST finds that a token * sequence matches a known place name (e.g., "New York" → locality + region), this module * produces additive biases that nudge the Viterbi decoder toward the matching BIO labels. * * Composes with the QueryShape prior via addEmissionMatrix — same integration point, same additive * semantics. * * SentencePiece ↔ FST bridge: SentencePiece pieces are grouped into whitespace words (by the ▁ * sentinel), normalized through the same pipeline as FST edges (NFKC, lowercase, strip * non-alnum), and walked through the FST as contiguous subpaths. * * Uses structural typing for the FST input so this module has zero dependencies on * `@mailwoman/resolver-wof-sqlite` — consumers pass an FSTMatcher instance, but this file only * consumes the shape. */ import type { TokenLike } from "./query-shape-prior.ts"; export interface FSTMatchLike { stateID: number; accepted: boolean; depth: number; } export interface FSTPlaceEntryLike { wofID: number; placetype: string; /** * REFERENTIAL likelihood in [0, 1] — population-anchored. The ONLY score the decoder bias reads (ROAD_TO_V9 §2, * ratified 2026-08-06: "the importance of a knowledge-base article is not the probability that this is the place the * user means"). * * The structural type deliberately does NOT name `encyclopedic`. The FST entries the matcher hands over carry it, and * a bias that could see it would eventually use it — so the seam is where the policy is enforced, not a comment. */ referential: number; } export interface FSTMatcherLike { walk(tokens: string[]): FSTMatchLike | null; walkFrom(prev: FSTMatchLike, token: string): FSTMatchLike | null; accepting(stateID: number): FSTPlaceEntryLike[]; } /** * The only placetypes that reach a BIO tag. * * Exported because a probe that reports "what bias would the decoder get for this surface" has to collapse the FST's * accepting entries the SAME way {@link applyBias} does, and a second copy of this map makes the probe answer a * question about a decoder that does not exist. A `county` or `borough` entry is walked, deduped, and dropped without * ever touching the emission matrix — no attested board row licenses a tag for those tiers, and `region` would be a * guess. * * `localadmin` and `neighbourhood` map to `locality` (the C4 census's one attested covering-surface class, #1747): * `Biggin Hill, United Kingdom` is accepted by the GB FST as a NEIGHBOURHOOD entry, and dropping it left the covering * surface with zero bias while the sub-span reading fragmented the parse (`locality: Biggin` + a stranded * `street_suffix: Hill`). `localadmin` is WOF's administrative twin of a locality — the resolver's placetype filter * groups already treat the pair as one contest class. The bias stays soft (referential-scaled), so a dependent-locality * reading can still win where the model's own emissions say so. */ export declare const PLACETYPE_TO_BIO: ReadonlyMap; /** * An FST entry as both the decoder and the probes read it. */ export interface FSTEntryLike { placetype: string; /** * The referential/importance score. Named loosely because the two probes and the prior reach it under different field * names on their own record types. */ importance: number; } /** * Collapse accepting entries to `max(importance)` PER BIO TAG — the only shape {@link applyBias} acts on. * * The per-place ranking INSIDE a name is invisible to the decoder; only the per-tag max is not. A caller reporting * anything finer would overstate what an importance change can do. * * An EMPTY result is not a zero bias: it means the surface was accepted but carries no BIO-mapped placetype, so the * decoder sees nothing. A caller must keep that apart from "the FST does not accept this surface at all", which is * absence, and from a tag present with value `0`, which is a measured zero. */ export declare function collapseFSTBias(entries: ReadonlyArray): Map; export interface WordGroup { fstToken: string; pieceIndices: number[]; } /** * One accepting contiguous FST path, before any emission-bias policy is applied. * * This is an observability shape: it reports every accepted surface, including nested matches. It does not deduplicate * WOF ids, rank matches, or mutate decoder emissions. */ export interface FSTAcceptedMatch { startPiece: number; endPiece: number; startWord: number; endWord: number; entries: FSTPlaceEntryLike[]; } /** * Enumerate every accepting contiguous FST path over the same reconstructed words used by * {@link buildFSTEmissionPriors}. * * `endPiece` and `endWord` are exclusive. Empty normalized word groups remain transparent while walking, matching the * prior's treatment of punctuation-only SentencePiece groups. The returned list preserves walk order: start word first, * then increasing end word. */ export declare function findFSTAcceptedMatches(fst: FSTMatcherLike, pieces: ReadonlyArray<{ piece: string; }>): FSTAcceptedMatch[]; /** * Match-length scaling mode for the importance bias (#1142). A single-token place match is weak evidence (a place name * that is also a surname / street head / common word); a multi-token match is reliable. `both` scales the positive * locality bias AND the street suppression by match length; `suppression` scales only the suppression (leaving the * positive bias intact — safe for the bare-fragment regime where the positive gazetteer bias earns its keep); `off` * disables it. */ export type ImportanceLengthScaleMode = "off" | "suppression" | "both"; /** * Street-context gate for the positive FST bias (#1142, street-context gate — the FR-fragment complement to #1173's * suppression length-scaling). * * Washington/Madison/Jackson are simultaneously the highest-importance US place names AND the commonest US street * names, so a positive locality/region bias must be withheld when the matched span sits in a syntactically * street-headed position — gated on SYNTAX (street-type adjacency, house-number-left), NEVER on the importance value * (`importance²` magnitude sharpening was measured and REJECTED: it re-imports exactly this collision). * Positive-evidence-only: the gate can only scale the positive bias DOWN when street context is present; its absence * never penalizes, and a parse with no street context is byte-identical to the ungated path. * * The street-type signal source is the street-morphology FST (`fst-street-morphology.bin`, locale-general — catches * prefix locales like "Rue de Rivoli", the FR −3), NOT codex `us/street-suffix.ts` (US-only — using it re-introduces an * FR regression). */ export interface StreetContextGateOpts { /** * The street-morphology FST matcher (same instance the street-morphology prior consumes). */ fst: FSTMatcherLike; /** * Multiplier applied to the positive `impBias` when the gate fires. Default 0.25 (tune 0.15–0.4). Deliberately NOT * zero — "New York Ave" still deserves some admin mass for the semi-markov decoder. */ positiveScale?: number; } export interface FSTPriorOpts { biasScale?: number; /** * Maximum bias magnitude (logits). Prevents large-population places from overriding the model. Default 3.0. */ maxBias?: number; suppressionScale?: number; /** * See {@link ImportanceLengthScaleMode}. Default `suppression` (measured best; see the caller). */ importanceLengthScaleMode?: ImportanceLengthScaleMode; /** * See {@link StreetContextGateOpts}. Absent → current behavior (default-safe no-op). */ streetContext?: StreetContextGateOpts; } /** * Build a `[seqLen][numLabels]` bias matrix from FST gazetteer matches. * * Walks all contiguous subpaths of the reconstructed whitespace-token sequence through the FST. For each accepting * state, biases the corresponding BIO labels on the matched pieces. */ export declare function buildFSTEmissionPriors(fst: FSTMatcherLike, pieces: ReadonlyArray, labels: ReadonlyArray, opts?: FSTPriorOpts): number[][]; /** * Group SentencePiece pieces into whitespace-delimited words. Each word's literal text is reconstructed by * concatenating pieces (minus leading ▁), then normalized through the same pipeline the FST builder uses. * * **The word boundary is `▁` (the SentencePiece space sentinel) — and ONLY `▁`.** The loop carries one piece of state, * `current: WordGroup | null` — the word presently being assembled, or `null` when a word is PENDING (nothing is open, * and the next real content should start one fresh, whatever piece it arrives on). Three kinds of piece, crossed with * that state, is the whole state machine: * * 1. **`▁`-prefixed, with alnum content** (a genuine new word, e.g. `"▁Stock"`, `"▁Tyne"`): always closes whatever * `current` holds (pushing it to `groups`) and opens a fresh one. This is the only case that unconditionally starts * a word — every other case below is conditioned on whether one is already open or pending. * 2. **`▁`-prefixed, NO alnum content** (a bare `"▁"` — a lone space tokenized as its own piece with nothing attached — or * a punctuation piece the tokenizer fused with its own leading space): closes whatever `current` holds, same as case * 1, but does NOT open a new word — it also gets its own empty placeholder group (`{ fstToken: "", pieceIndices: [i] * }`, preserving index alignment) and leaves the state PENDING (`current = null`) for whatever piece comes next. * 3. **Not `▁`-prefixed** (interior to whatever's already true — nothing here is itself a boundary): * * - **Alnum** (a SentencePiece subword split, e.g. `"ton"` after `"▁Stock"`): if a word is open (`current` is non-null), * this is an ordinary continuation — appended onto it. If a word is PENDING (`current` is `null` — because the last * piece was case 2's bare `▁`, or a run of case-3-punctuation with nothing to attach to, or this is the very first * piece), this piece is the actual start of the pending word: nothing else marks the boundary, so it opens `current` * fresh here instead of being dropped. **Opening on a non-`▁` piece is load-bearing, not a nicety**: restrict * word-opening to `▁`-prefixed pieces (or `i === 0`) and a pending word whose first piece happens to lack its own `▁` * vanishes silently — that is the exact shape a SentencePiece vocab produces for a short/common word never learned as * a merged `"▁word"` token (`"on"`, `"upon"`, `"super"`, bare `"IL"` after a lone `"▁"` before it — all observed on * the production `v0.9.0-multisplice` tokenizer, so not a fixture-vocab quirk). * - **Punctuation-only** (`"-"`, `"'"`, a bare `","`): if a word is open, it's interior punctuation — absorbed into * `current.pieceIndices` (contributing nothing to `fstToken`; `normalizeFSTToken` strips punctuation anyway) but * never resetting it, so the pieces that follow still have a `current` to land on ("Stockton-on-Tees", "Bishop's * Stortford"). If a word is PENDING, this punctuation piece has nothing to attach to either — same empty-placeholder * treatment as case 2 — and the state stays PENDING; the punctuation doesn't consume or clear the pending word, it * just has nothing of its own to open. * * The pending state is what keeps `"Stockton , Lancashire"` from fusing "Stockton" and "Lancashire" into one group: * however many raw empty-placeholder groups the comma/space sequence produces (one from the bare `▁`, one from the * comma itself if it too has no leading `▁`), they never carry real content, so non-empty-filtering callers * (`placetype-pair-prior.ts`'s window builder) still see "stockton" and "lancashire" as two separate, * non-adjacent-fused entries. * * Exported (alongside {@linkcode normalizeFSTToken} and the {@linkcode WordGroup} type) so consumers like the * street-morphology prior can reuse the same piece-grouping/normalization pipeline without duplication. Internal helper * signature; not part of the public neural API. */ export declare function groupPiecesIntoWords(pieces: ReadonlyArray<{ piece: string; }>): WordGroup[]; /** * Normalize a whitespace word to FST-index form: NFKC → lowercase → strip punctuation and symbols. * * NFKC (compatibility decomposition + canonical composition) unifies ligatures, superscripts, and other decomposable * forms; it does NOT strip diacritics ("Álava" stays "álava", not "alava"). Both the FST builder and this runtime fold * use the same pipeline, so any index built from either is consistent — that consistency is the guarantee, not the * specific form (indexed and query surfaces agree on diacritics). * * The regex `\p{P}\p{S}` strips all Unicode punctuation and symbols (categories P and S), leaving spaces intact — space * (U+0020) is Unicode category Zs (separator), NOT matched by `\p{P}` or `\p{S}`. So this function preserves spaces * within the token string ("Stockton on Tees" → "stockton on tees"). The hyphen/space EQUIVALENCE that produces * "stocktonontees" is a property of the caller's split-then-join pipeline in `groupPiecesIntoWords` — each word is * normalized separately, then words are joined with no separator. */ export declare function normalizeFSTToken(s: string): string; //# sourceMappingURL=fst-prior.d.ts.map