/** * @copyright Sister Software * @license AGPL-3.0 * @author Teffen Ellis, et al. * * The #244 coarse-placer: a tiny always-resident linear classifier over hashed char-n-gram + script * features ({@link featurize}). Maps an address string → a coarse country/region with a * TEMPERATURE-CALIBRATED confidence, and ABSTAINS below a threshold ("probably off my loaded * map") rather than emit a confident mis-placement. Pure + dependency-free — runs in node and the * browser. */ export { COARSE_CLASSES, FEATURE_DIM, featurize } from "./featurize.ts"; export interface CoarsePlacerArtifact { /** * The coarse placer's classes (the country/region codes it routes to). */ classes: readonly string[]; featureDim: number; /** * Temperature for confidence calibration (logits are divided by this before softmax). */ temperature: number; /** * Bias vector. The bias is the log-prior of each class (the model's belief before seeing any input). The bias is * learned during training, and the temperature is fit on the validation set to calibrate the confidence. The bias is * added to the weighted sum of features for each class before applying the softmax. */ bias: number[]; /** * Flat row-major weight matrix, length `classes.length * featureDim`. */ weights: Float32Array; } /** * On-disk `meta.json` shape. The fp32 artifact omits `quantization`/`scales`; the int8 artifact (from * `scripts/coarse-placer/quantize.mjs`) sets `quantization: "int8-per-row"` and carries one `scale` per class, so * `weights.bin` can be a 4×-smaller `Int8Array` dequantized as `int8 * scales[class]`. */ export interface CoarsePlacerMeta { classes: string[]; featureDim: number; temperature: number; bias: number[]; quantization?: "int8-per-row"; /** * Per-class dequantization scale; present iff `quantization === "int8-per-row"`. */ scales?: number[]; } /** * Dequantize a per-row int8 weight matrix back to fp32: `W[c][i] = int8[c*dim + i] * scales[c]`. The predict path stays * fp32 (identical math); quantization only shrinks the serialized/wire artifact. Pure — usable in the browser loader * too. */ export declare function dequantizeInt8Weights(int8: Int8Array, scales: readonly number[], classCount: number, dim: number): Float32Array; export interface CoarsePrediction { /** * The predicted class, or `null` when the model abstained (confidence below the threshold). */ country: string | null; /** * Calibrated probability of the top class (the abstention signal). */ confidence: number; abstained: boolean; /** * The full calibrated class distribution. */ probs: Record; } export interface CoarsePlacerOpts { /** * Abstain when the calibrated top-class confidence is below this (default 0.5). */ abstainBelow?: number; /** * Open-set reject rule (#244 M2). When `true`, the ABSTAIN decision uses the total IN-MAP probability mass `1 - * P(OTHER)` instead of the single top-class prob, and a KEEP routes to the argmax IN-MAP class (never `OTHER`). This * decouples "is it in-map at all?" (the reject question) from "which country?" (the routing question) — so a * clearly-in-map-but-country-ambiguous address (mass split across several in-map countries) is KEPT rather than * wrongly rejected. It clears the 90/90 the default max-prob rule cannot (post-hoc, no retrain: heldout-family * generalization 89→91 — see docs/articles/evals/resolver-geo/2026-06-14-coarse-placer-m2-openset.md). The returned * `confidence` becomes the routed in-map country's marginal probability (the soft-prior posterior weight). Default * `false` = the M1 max-prob rule (byte-stable; can still return `OTHER`). */ openSet?: boolean; } export declare class CoarsePlacer { #private; constructor(artifact: CoarsePlacerArtifact, opts?: CoarsePlacerOpts); /** * Load a placer from an artifact directory holding `meta.json` + `weights.bin` (the layout * `scripts/coarse-placer/train.mjs` and `quantize.mjs` write). Handles both the fp32 artifact (`weights.bin` is a * `Float32Array`) and the int8 artifact (`meta.quantization === "int8-per-row"`, `weights.bin` is an `Int8Array` * dequantized via `meta.scales`). Node-only — the `node:` imports are dynamic so bundling the class for the browser * doesn't pull them in. */ static fromArtifactDir(dir: string, opts?: CoarsePlacerOpts): Promise; /** * Load the int8 model bundled in `@mailwoman/core` (`core/data/coarse-placer/`). Node-only — uses the package path * builder (the #481-corrected `__isCompiledTree` makes this resolve to the shipped `data/` in source, compiled, AND * installed-package layouts). Override the directory with `$MAILWOMAN_COARSE_PLACER_DIR`. Callers set `abstainBelow` * per their use (the soft-country-prior wiring passes 0.9 — see * docs/articles/plan/2026-06-14-coarse-placer-soft-signal-spec.md). */ static fromBundled(opts?: CoarsePlacerOpts): Promise; predict(text: string): CoarsePrediction; } /** * The coarse placer's country POSTERIOR, shaped for the resolver: a per-country probability map like `{GB: 0.8, FR: * 0.06}` — "given this address text, how likely is each country?" ("posterior" in the Bayesian sense: the model's * belief AFTER seeing the input; see the glossary). Every in-map class except `OTHER` is included; returns `null` when * the model abstained or routed off-map. The resolver consumes it as `anchorPosterior`: each candidate's rank gains * `anchorWeight × posterior[candidate.country]`, so EVERY plausible country is boosted proportionally, and * country-ambiguous inputs (mass split DK↔NO) let the resolver's own place evidence break the tie — strictly more * informative than committing to the single argmax. Values are raw marginals in [0, 1] (un-renormalized; they sum to * the in-map mass `1 − P(OTHER)`), matching the one-hot `confidence` scale so `anchorWeight` needs no retuning. */ export declare function inMapPosterior(prediction: CoarsePrediction, opts?: { /** * Epsilon floor (see the glossary): drop countries whose probability falls below this cutoff before the resolver * sees the posterior, so implausible tails cannot influence ranking. Domain [0, 1]: `0` (the DEFAULT) passes the * full distribution through unchanged; raising it keeps only stronger beliefs — at the extreme only the argmax * survives (a one-hot). The default is 0 deliberately: the #928 investigation swept 0.05–0.30 against the misroute * battery and every value was byte-identical (the drift's real cause was the anchor re-rank's score key, fixed * separately) — no nonzero default has a measured basis, and the shipped distribution contract stays * byte-identical. The knob exists for distribution-mode experiments (`--posterior-floor` on the misroute eval). */ epsilonFloor?: number; }): Record | null; /** * Load a coarse-placer from a JSON metadata file + a sibling `.weights.bin` (Float32). */ export declare function loadCoarsePlacer(metaJson: { classes: string[]; featureDim: number; temperature: number; bias: number[]; }, weights: Float32Array, opts?: CoarsePlacerOpts): Promise; //# sourceMappingURL=coarse-placer.d.ts.map