mailwoman 8.0.0 → 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api-engine.ts +7 -3
- package/commands/gazetteer/build/fst.tsx +60 -0
- package/commands/gazetteer/build/street-morphology.tsx +54 -0
- package/commands/geocode.tsx +4 -3
- package/commands/situs/interpolation-shard.tsx +23 -1
- package/eval-harness/parity-corpus.ts +7 -4
- package/gazetteer-pipeline/coverage-manifest.ts +163 -0
- package/gazetteer-pipeline/fst.ts +213 -0
- package/gazetteer-pipeline/index.ts +9 -1
- package/gazetteer-pipeline/pair-index-hierarchy-probe.ts +310 -0
- package/gazetteer-pipeline/pair-index-hierarchy-verify.ts +278 -0
- package/gazetteer-pipeline/street-morphology.ts +94 -0
- package/geocode-core.ts +43 -15
- package/out/api-engine.d.ts.map +1 -1
- package/out/api-engine.js +7 -2
- package/out/api-engine.js.map +1 -1
- package/out/commands/gazetteer/build/fst.d.ts +22 -0
- package/out/commands/gazetteer/build/fst.d.ts.map +1 -0
- package/out/commands/gazetteer/build/fst.js +42 -0
- package/out/commands/gazetteer/build/fst.js.map +1 -0
- package/out/commands/gazetteer/build/street-morphology.d.ts +22 -0
- package/out/commands/gazetteer/build/street-morphology.d.ts.map +1 -0
- package/out/commands/gazetteer/build/street-morphology.js +47 -0
- package/out/commands/gazetteer/build/street-morphology.js.map +1 -0
- package/out/commands/geocode.d.ts.map +1 -1
- package/out/commands/geocode.js +4 -3
- package/out/commands/geocode.js.map +1 -1
- package/out/commands/situs/interpolation-shard.d.ts.map +1 -1
- package/out/commands/situs/interpolation-shard.js +21 -1
- package/out/commands/situs/interpolation-shard.js.map +1 -1
- package/out/eval-harness/parity-corpus.d.ts.map +1 -1
- package/out/eval-harness/parity-corpus.js +7 -4
- package/out/eval-harness/parity-corpus.js.map +1 -1
- package/out/gazetteer-pipeline/coverage-manifest.d.ts +59 -0
- package/out/gazetteer-pipeline/coverage-manifest.d.ts.map +1 -0
- package/out/gazetteer-pipeline/coverage-manifest.js +141 -0
- package/out/gazetteer-pipeline/coverage-manifest.js.map +1 -0
- package/out/gazetteer-pipeline/fst.d.ts +68 -0
- package/out/gazetteer-pipeline/fst.d.ts.map +1 -0
- package/out/gazetteer-pipeline/fst.js +165 -0
- package/out/gazetteer-pipeline/fst.js.map +1 -0
- package/out/gazetteer-pipeline/index.d.ts +3 -1
- package/out/gazetteer-pipeline/index.d.ts.map +1 -1
- package/out/gazetteer-pipeline/index.js +9 -1
- package/out/gazetteer-pipeline/index.js.map +1 -1
- package/out/gazetteer-pipeline/pair-index-hierarchy-probe.d.ts +81 -0
- package/out/gazetteer-pipeline/pair-index-hierarchy-probe.d.ts.map +1 -0
- package/out/gazetteer-pipeline/pair-index-hierarchy-probe.js +224 -0
- package/out/gazetteer-pipeline/pair-index-hierarchy-probe.js.map +1 -0
- package/out/gazetteer-pipeline/pair-index-hierarchy-verify.d.ts +31 -0
- package/out/gazetteer-pipeline/pair-index-hierarchy-verify.d.ts.map +1 -0
- package/out/gazetteer-pipeline/pair-index-hierarchy-verify.js +225 -0
- package/out/gazetteer-pipeline/pair-index-hierarchy-verify.js.map +1 -0
- package/out/gazetteer-pipeline/street-morphology.d.ts +49 -0
- package/out/gazetteer-pipeline/street-morphology.d.ts.map +1 -0
- package/out/gazetteer-pipeline/street-morphology.js +63 -0
- package/out/gazetteer-pipeline/street-morphology.js.map +1 -0
- package/out/geocode-core.d.ts +17 -6
- package/out/geocode-core.d.ts.map +1 -1
- package/out/geocode-core.js +19 -8
- package/out/geocode-core.js.map +1 -1
- package/out/runtime-pipeline.d.ts +3 -2
- package/out/runtime-pipeline.d.ts.map +1 -1
- package/out/runtime-pipeline.js +17 -11
- package/out/runtime-pipeline.js.map +1 -1
- package/package.json +23 -23
- package/runtime-pipeline.ts +22 -13
package/api-engine.ts
CHANGED
|
@@ -332,12 +332,16 @@ export async function createServeEngine(): Promise<ServeEngine> {
|
|
|
332
332
|
...incomingOpts,
|
|
333
333
|
defaultCountry: incomingOpts.defaultCountry ?? deps.defaultCountry,
|
|
334
334
|
...(addressPoints ? { addressPoints } : {}),
|
|
335
|
+
// #374 calibration ladder: explicit incoming factor (instrument override, survives the spread) →
|
|
336
|
+
// the artifact's own header value (`interpolation.radiusCalibration`, read at shard open — the
|
|
337
|
+
// resolver consumes it directly, nothing passed here) → the in-code per-region table for shards
|
|
338
|
+
// predating the `interp_calibration` metadata table.
|
|
335
339
|
...(interpolation
|
|
336
340
|
? {
|
|
337
341
|
interpolation,
|
|
338
|
-
interpolationRadiusCalibration
|
|
339
|
-
|
|
340
|
-
|
|
342
|
+
...(incomingOpts.interpolationRadiusCalibration == null && interpolation.radiusCalibration == null
|
|
343
|
+
? { interpolationRadiusCalibration: interpCalibrationForRegion(INTERP_RADIUS_CALIBRATION, slug) }
|
|
344
|
+
: {}),
|
|
341
345
|
}
|
|
342
346
|
: {}),
|
|
343
347
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `mailwoman gazetteer build fst` — per-locale decode-bias FST gazetteers (`fst-<locale>.bin`,
|
|
7
|
+
* #1318) with build-time degenerate-surface curation. Artifacts land in a staging dir BESIDE the
|
|
8
|
+
* shipped `fst-per-locale/` (never overwriting); the swap is operator-gated after the battery. See
|
|
9
|
+
* `mailwoman/gazetteer-pipeline/fst.ts` for the curation policy.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Text } from "ink"
|
|
13
|
+
import zod from "zod"
|
|
14
|
+
|
|
15
|
+
import { type CommandComponent, useCommandTask } from "../../../cli-kit/index.ts"
|
|
16
|
+
|
|
17
|
+
const OptionsSchema = zod.object({
|
|
18
|
+
locales: zod.string().optional().describe("Comma-separated locales (default: all shipped FST locales)"),
|
|
19
|
+
db: zod.string().optional().describe("WOF admin DB (default: $MAILWOMAN_DATA_ROOT/wof/admin-global-priority.db)"),
|
|
20
|
+
output: zod.string().optional().describe("Output dir (default: $MAILWOMAN_DATA_ROOT/wof/fst-per-locale-curated)"),
|
|
21
|
+
uncurated: zod.boolean().default(false).describe("A/B control build: same DB, no curation"),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export { OptionsSchema as options }
|
|
25
|
+
|
|
26
|
+
const GazetteerBuildFST: CommandComponent<typeof OptionsSchema> = ({ options }) => {
|
|
27
|
+
const state = useCommandTask(async () => {
|
|
28
|
+
const { buildLocaleFSTs } = await import("../../../gazetteer-pipeline/fst.ts")
|
|
29
|
+
const built = buildLocaleFSTs({
|
|
30
|
+
locales: options.locales?.split(",").map((s) => s.trim()),
|
|
31
|
+
dbPath: options.db,
|
|
32
|
+
outputDir: options.output,
|
|
33
|
+
uncurated: options.uncurated,
|
|
34
|
+
onProgress: (line) => console.error(line),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
return built.map(
|
|
38
|
+
(b) =>
|
|
39
|
+
`fst-${b.locale} → ${b.path} (${(b.bytes / 1e6).toFixed(1)} MB, ${b.nameInsertions} insertions, ${b.excludedInsertions} excluded)`
|
|
40
|
+
)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
if (state.status === "error") return <Text color="red">✗ {state.message}</Text>
|
|
44
|
+
|
|
45
|
+
if (state.status === "done") {
|
|
46
|
+
return (
|
|
47
|
+
<>
|
|
48
|
+
{state.result.map((line, i) => (
|
|
49
|
+
<Text key={i} color="green">
|
|
50
|
+
✓ {line}
|
|
51
|
+
</Text>
|
|
52
|
+
))}
|
|
53
|
+
</>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default GazetteerBuildFST
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `mailwoman gazetteer build street-morphology` — the sealed street-morphology FST artifact
|
|
7
|
+
* (`fst-street-morphology.bin`), the #1315 street-context gate's signal source serialized once at
|
|
8
|
+
* build time instead of rebuilt from the libpostal dictionaries per process (and never in the
|
|
9
|
+
* browser). Lands at `$MAILWOMAN_DATA_ROOT/wof/` by default — beside, never inside, the
|
|
10
|
+
* per-locale FST dir. See `mailwoman/gazetteer-pipeline/street-morphology.ts` for the rationale.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Text } from "ink"
|
|
14
|
+
import zod from "zod"
|
|
15
|
+
|
|
16
|
+
import { type CommandComponent, useCommandTask } from "../../../cli-kit/index.ts"
|
|
17
|
+
|
|
18
|
+
const OptionsSchema = zod.object({
|
|
19
|
+
dictionaries: zod
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("libpostal dictionaries root (default: core's bundled data/libpostal/dictionaries)"),
|
|
23
|
+
locales: zod
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Comma-separated locale subfolders (default: every locale with a street_types.txt)"),
|
|
27
|
+
output: zod.string().optional().describe("Output path (default: $MAILWOMAN_DATA_ROOT/wof/fst-street-morphology.bin)"),
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export { OptionsSchema as options }
|
|
31
|
+
|
|
32
|
+
const GazetteerBuildStreetMorphology: CommandComponent<typeof OptionsSchema> = ({ options }) => {
|
|
33
|
+
const state = useCommandTask(async () => {
|
|
34
|
+
const { buildStreetMorphologyArtifact } = await import("../../../gazetteer-pipeline/street-morphology.ts")
|
|
35
|
+
const built = buildStreetMorphologyArtifact({
|
|
36
|
+
dictionariesDir: options.dictionaries,
|
|
37
|
+
locales: options.locales?.split(",").map((s) => s.trim()),
|
|
38
|
+
output: options.output,
|
|
39
|
+
onProgress: (line) => console.error(line),
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
return `${built.path} (${(built.bytes / 1e3).toFixed(0)} kB, ${built.canonicalCount} canonicals, ${built.variantCount} variants, ${built.localeCount} locales)`
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
if (state.status === "error") return <Text color="red">✗ {state.message}</Text>
|
|
46
|
+
|
|
47
|
+
if (state.status === "done") {
|
|
48
|
+
return <Text color="green">✓ {state.result}</Text>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default GazetteerBuildStreetMorphology
|
package/commands/geocode.tsx
CHANGED
|
@@ -120,9 +120,10 @@ const OptionsSchema = zod.object({
|
|
|
120
120
|
.optional()
|
|
121
121
|
.describe(
|
|
122
122
|
"Conformal calibration multiplier for the interpolation tier's reported uncertainty_m (#374). " +
|
|
123
|
-
"The raw half-segment radius covers only ~72% of true errors. Default (unset): the
|
|
124
|
-
"
|
|
125
|
-
"
|
|
123
|
+
"The raw half-segment radius covers only ~72% of true errors. Default (unset): the shard's own " +
|
|
124
|
+
"baked value (its interp_calibration metadata table) when it carries one, else the in-code " +
|
|
125
|
+
"per-region table (#584) selected by parsed region — 1.44 (DC) … 3.12 (AZ), 1.95 for unmeasured " +
|
|
126
|
+
"states — for a ~90% bound. Pass an explicit number to force a single multiplier everywhere (1 = raw)."
|
|
126
127
|
),
|
|
127
128
|
placeCountry: zod
|
|
128
129
|
.boolean()
|
|
@@ -35,6 +35,15 @@ import { Box, Text } from "ink"
|
|
|
35
35
|
import zod from "zod"
|
|
36
36
|
|
|
37
37
|
import { type CommandComponent, commandError, useCommandTask } from "../../cli-kit/index.ts"
|
|
38
|
+
import { INTERP_RADIUS_CALIBRATION } from "../../interp-calibration.ts"
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Provenance tag for the baked `interp_calibration` row — the split-conformal multi-region recalibration this build
|
|
42
|
+
* selects its multiplier from (`docs/articles/evals/calibration/2026-06-14-interp-multiregion-recalibration.md`). Bump
|
|
43
|
+
* when the calibration source of record (`interp-calibration.ts` / `data/calibration/interp-radius-conformal.json`) is
|
|
44
|
+
* re-measured.
|
|
45
|
+
*/
|
|
46
|
+
const CALIBRATION_METHOD = "split-conformal:2026-06-14"
|
|
38
47
|
|
|
39
48
|
/** State abbreviation → state FIPS prefix, for picking county files out of --edges-dir. */
|
|
40
49
|
const STATE_FIPS: Record<string, string> = {
|
|
@@ -183,7 +192,8 @@ const SitusInterpolationShard: CommandComponent<typeof OptionsSchema> = ({ optio
|
|
|
183
192
|
"@duckdb/node-api is not installed — `situs interpolation-shard` is a maintainer-only data command"
|
|
184
193
|
)
|
|
185
194
|
}
|
|
186
|
-
const { STREET_SEGMENT_COLUMNS, createStreetSegmentTable, createStreetSegmentIndexes } =
|
|
195
|
+
const { STREET_SEGMENT_COLUMNS, createStreetSegmentTable, createStreetSegmentIndexes, writeInterpCalibration } =
|
|
196
|
+
segmentSchema
|
|
187
197
|
const { canonicalizeRouteKey, normalizeStreetForKey } = streetNormalize
|
|
188
198
|
|
|
189
199
|
const shapefiles = globSync(`${options.edgesDir}/tl_*_${STATE_FIPS[STATE]}???_edges.shp`).sort()
|
|
@@ -209,6 +219,17 @@ const SitusInterpolationShard: CommandComponent<typeof OptionsSchema> = ({ optio
|
|
|
209
219
|
// producer can't drift. DuckDB below is the raw spatial reader; the hot INSERT stays on `db`.
|
|
210
220
|
const kdb = new DatabaseClient<StreetSegmentDatabase>({ database: db })
|
|
211
221
|
await createStreetSegmentTable(kdb)
|
|
222
|
+
// #374 doctrine: the conformal radius multiplier is a property of the calibration set, so it ships IN
|
|
223
|
+
// the artifact — bake the state's factor (or the conservative default for unmeasured states) into the
|
|
224
|
+
// shard's `interp_calibration` metadata table. `StreetInterpolator` reads it at open time; callers
|
|
225
|
+
// stop carrying the number.
|
|
226
|
+
const measuredMultiplier = INTERP_RADIUS_CALIBRATION.byRegion[STATE]
|
|
227
|
+
const calibration = {
|
|
228
|
+
radius_multiplier: measuredMultiplier ?? INTERP_RADIUS_CALIBRATION.default,
|
|
229
|
+
method: CALIBRATION_METHOD,
|
|
230
|
+
region: measuredMultiplier === undefined ? "default" : STATE,
|
|
231
|
+
}
|
|
232
|
+
await writeInterpCalibration(kdb, calibration)
|
|
212
233
|
const insert = db.prepare(
|
|
213
234
|
`INSERT INTO street_segment (${STREET_SEGMENT_COLUMNS.join(", ")})
|
|
214
235
|
VALUES (${STREET_SEGMENT_COLUMNS.map(() => "?").join(", ")})`
|
|
@@ -300,6 +321,7 @@ const SitusInterpolationShard: CommandComponent<typeof OptionsSchema> = ({ optio
|
|
|
300
321
|
`distinct streets: ${stats.streets} · postcodes: ${stats.postcodes}`,
|
|
301
322
|
`parity: odd ${parityCounts.odd} · even ${parityCounts.even} · mixed ${parityCounts.mixed}`,
|
|
302
323
|
`skipped non-numeric ranges: ${skippedNonNumeric}`,
|
|
324
|
+
`baked radius calibration: ×${calibration.radius_multiplier} (${calibration.region}, ${calibration.method})`,
|
|
303
325
|
]
|
|
304
326
|
})
|
|
305
327
|
|
|
@@ -98,11 +98,14 @@ export async function runParityEval(options: ParityEvalOptions = {}): Promise<Pa
|
|
|
98
98
|
let fstStreetMorphology: import("@mailwoman/resolver-wof-sqlite/fst-matcher").FSTMatcher | undefined
|
|
99
99
|
|
|
100
100
|
if (options.streetMorphology) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
// Sealed-artifact-first (static-index candidate 1): the loader's shared ladder — data-root
|
|
102
|
+
// `fst-street-morphology.bin`, degrading to the per-process dictionary build this site used to
|
|
103
|
+
// inline (with a cwd-relative dictionaries path, no less).
|
|
104
|
+
const { loadStreetMorphologyFST } = await import("@mailwoman/resolver-wof-sqlite/street-morphology-fst-loader")
|
|
105
|
+
const loaded = loadStreetMorphologyFST({ onWarn: (message) => console.warn(message) })
|
|
106
|
+
fstStreetMorphology = loaded.matcher
|
|
104
107
|
console.log(
|
|
105
|
-
`street-morphology bias ON
|
|
108
|
+
`street-morphology bias ON (${loaded.source === "artifact" ? `sealed artifact ${loaded.path}` : "per-process dictionary build"}${loaded.provenance ? `: ${loaded.provenance.placeCount} canonical affixes, ${loaded.provenance.nameInsertions} variant insertions` : ""})`
|
|
106
109
|
)
|
|
107
110
|
}
|
|
108
111
|
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The MEASURED COVERAGE RECORD for the candidate gazetteer + its build-time emission (survey
|
|
7
|
+
* candidate #2, 2026-07-26) — the durable home for two sets of facts that used to be hand-grown
|
|
8
|
+
* code constants updated by PR-after-someone-remembers:
|
|
9
|
+
*
|
|
10
|
+
* - The hard-country-filter coverage measurements (#743/#194) behind
|
|
11
|
+
* `HARD_PLACE_COUNTRY_SAFELIST` (`core/pipeline/runtime-pipeline.ts`) — previously a code
|
|
12
|
+
* comment ("US 100, FR 100 … FI 69.5 (out)"), i.e. measurement as trivia.
|
|
13
|
+
* - The guard-B plausibility boxes behind `COUNTRY_BBOX` (`resolver/plausibility.ts`).
|
|
14
|
+
*
|
|
15
|
+
* Doctrine (operator-ratified 2026-07-26): facts ABOUT an artifact live in the artifact's
|
|
16
|
+
* manifest, read at load — so they update at gazetteer REBUILD, not at a code PR. This module is
|
|
17
|
+
* the drawer: it owns the reviewed measurement record ({@link MEASURED_COUNTRY_COVERAGE},
|
|
18
|
+
* {@link MEASURED_COUNTRY_BBOXES} — grow THESE at promotes, like `defaults.ts` owns the build
|
|
19
|
+
* recipe) and the emission step `buildCandidate` runs before sealing. The schema + canonical
|
|
20
|
+
* read/write functions live in `@mailwoman/resolver-wof-sqlite/coverage-manifest-schema` (the
|
|
21
|
+
* fold/build convention: canonical package functions, composed here).
|
|
22
|
+
*
|
|
23
|
+
* The SHIPPED candidate gazetteer is never patched ("never patch databases — rebuild"): an
|
|
24
|
+
* artifact predating the manifest reads `undefined` at open and every consumer falls back to the
|
|
25
|
+
* code constants byte-identically. The meaning-of-zero rule is honored structurally: FI/PL are
|
|
26
|
+
* PRESENT rows with `hardFilterSafe: false` (measured, failed the gate) — distinguishable from a
|
|
27
|
+
* country that was simply never measured (absent row).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { DatabaseSync } from "node:sqlite"
|
|
31
|
+
|
|
32
|
+
import { DatabaseClient } from "@mailwoman/core/kysley/client"
|
|
33
|
+
import type { CountryBBoxFact, CountryCoverageFact } from "@mailwoman/core/resolver"
|
|
34
|
+
// resolver-wof-sqlite is an optional peer of mailwoman (the geocode.tsx convention) — runtime
|
|
35
|
+
// imports are DYNAMIC inside the functions; type-only imports are erased and safe at module level.
|
|
36
|
+
import type { GazetteerCoverageDatabase } from "@mailwoman/resolver-wof-sqlite"
|
|
37
|
+
|
|
38
|
+
/** Shared source string for the #743 promote measurements. */
|
|
39
|
+
const OA_PANEL_SOURCE = "#743 OA held-out hard-resolve panel (DeepSeek-advised gate, 2026-06-22)"
|
|
40
|
+
|
|
41
|
+
/** Shared source string for the #928 promote OSM panels. */
|
|
42
|
+
const OSM_PANEL_SOURCE = "#928 promote OSM panel, night 34 (2026-07-06)"
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The reviewed per-country hard-filter coverage record — every promote-gate verdict + measurement that grew (or
|
|
46
|
+
* deliberately kept a country off) the hard-country safelist. This is the structured form of the receipts that lived in
|
|
47
|
+
* the `HARD_PLACE_COUNTRY_SAFELIST` code comment; the derived safelist (`hardFilterSafe === true`) is asserted
|
|
48
|
+
* byte-identical to that constant in `coverage-manifest.test.ts`, so the two cannot drift silently.
|
|
49
|
+
*
|
|
50
|
+
* Grow THIS at promotes (with the panel receipt in `source`); the fact reaches production at the next gazetteer rebuild
|
|
51
|
+
* — the constant in core is only the fallback for artifacts predating the manifest.
|
|
52
|
+
*/
|
|
53
|
+
export const MEASURED_COUNTRY_COVERAGE: readonly CountryCoverageFact[] = [
|
|
54
|
+
{ country: "US", hardFilterSafe: true, hardResolveRate: 1.0, measuredAt: "2026-06-22", source: OA_PANEL_SOURCE },
|
|
55
|
+
{ country: "FR", hardFilterSafe: true, hardResolveRate: 1.0, measuredAt: "2026-06-22", source: OA_PANEL_SOURCE },
|
|
56
|
+
{ country: "DE", hardFilterSafe: true, hardResolveRate: 1.0, measuredAt: "2026-06-22", source: OA_PANEL_SOURCE },
|
|
57
|
+
{ country: "ES", hardFilterSafe: true, hardResolveRate: 0.998, measuredAt: "2026-06-22", source: OA_PANEL_SOURCE },
|
|
58
|
+
{ country: "NL", hardFilterSafe: true, hardResolveRate: 0.973, measuredAt: "2026-06-22", source: OA_PANEL_SOURCE },
|
|
59
|
+
{ country: "IT", hardFilterSafe: true, hardResolveRate: 0.968, measuredAt: "2026-06-22", source: OA_PANEL_SOURCE },
|
|
60
|
+
// Measured and FAILED the gate — present rows on purpose (meaning-of-zero: a failed measurement is a
|
|
61
|
+
// first-class negative result, distinguishable from "never measured"). They stay on the soft prior
|
|
62
|
+
// until their gazetteer coverage is filled (#193).
|
|
63
|
+
{ country: "FI", hardFilterSafe: false, hardResolveRate: 0.695, measuredAt: "2026-06-22", source: OA_PANEL_SOURCE },
|
|
64
|
+
{ country: "PL", hardFilterSafe: false, hardResolveRate: 0.778, measuredAt: "2026-06-22", source: OA_PANEL_SOURCE },
|
|
65
|
+
// #928 promote (2026-07-06): the postcodeCountryPrior FORMAT signal routes GB/CA confidently (the
|
|
66
|
+
// language placer conflated both with US), and the OSM-panel gates passed with the hard filter ON.
|
|
67
|
+
// Rates here are the panels' RESOLVE rates (1 − unresolved/n): GB 293/300 (271 ok, 7 unresolved),
|
|
68
|
+
// CA 269/300 (200 ok, 31 unresolved) — CA cleared on the format-prior rationale despite the sub-95%
|
|
69
|
+
// panel number, which is exactly why `hardFilterSafe` is a stored VERDICT, not a rate threshold.
|
|
70
|
+
{
|
|
71
|
+
country: "GB",
|
|
72
|
+
hardFilterSafe: true,
|
|
73
|
+
hardResolveRate: 0.977,
|
|
74
|
+
sampleSize: 300,
|
|
75
|
+
measuredAt: "2026-07-06",
|
|
76
|
+
source: OSM_PANEL_SOURCE,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
country: "CA",
|
|
80
|
+
hardFilterSafe: true,
|
|
81
|
+
hardResolveRate: 0.897,
|
|
82
|
+
sampleSize: 300,
|
|
83
|
+
measuredAt: "2026-07-06",
|
|
84
|
+
source: OSM_PANEL_SOURCE,
|
|
85
|
+
},
|
|
86
|
+
// AU added with the #244 AU placer class: 150k-row G-NAF training → AU test-acc 100%, and the hard
|
|
87
|
+
// filter is recall-SAFE on the AU panel (unresolved 4→2 while abroad 43→20). No single-rate number
|
|
88
|
+
// in the receipt → no `hardResolveRate` (never invent a magnitude).
|
|
89
|
+
{
|
|
90
|
+
country: "AU",
|
|
91
|
+
hardFilterSafe: true,
|
|
92
|
+
measuredAt: "2026-07-06",
|
|
93
|
+
source: "#244 AU placer-class promote (2026-07-06): hard filter recall-safe (unresolved 4→2, abroad 43→20)",
|
|
94
|
+
},
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
/** Shared source string for the guard-B boxes. */
|
|
98
|
+
const BBOX_SOURCE = "2026-07-15 coordinate-parity receipt harness (scratchpad/coord-parity.mjs) — deliberately coarse"
|
|
99
|
+
|
|
100
|
+
const bbox = (country: string, latMin: number, latMax: number, lonMin: number, lonMax: number): CountryBBoxFact => ({
|
|
101
|
+
country,
|
|
102
|
+
latMin,
|
|
103
|
+
latMax,
|
|
104
|
+
lonMin,
|
|
105
|
+
lonMax,
|
|
106
|
+
source: BBOX_SOURCE,
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The reviewed guard-B bounding-box record — the structured form of `COUNTRY_BBOX` (`resolver/plausibility.ts`),
|
|
111
|
+
* asserted byte-identical to that constant in `coverage-manifest.test.ts`. Same growth discipline as
|
|
112
|
+
* {@link MEASURED_COUNTRY_COVERAGE}: add boxes HERE; the constant is only the pre-manifest fallback.
|
|
113
|
+
*/
|
|
114
|
+
export const MEASURED_COUNTRY_BBOXES: readonly CountryBBoxFact[] = [
|
|
115
|
+
bbox("US", 18, 72, -180, -66),
|
|
116
|
+
bbox("AU", -44, -10, 112, 154),
|
|
117
|
+
bbox("BR", -34, 6, -74, -34),
|
|
118
|
+
bbox("CZ", 48, 51.5, 12, 19),
|
|
119
|
+
bbox("DE", 47, 55.5, 5.5, 15.5),
|
|
120
|
+
bbox("ES", 35, 44, -10, 5),
|
|
121
|
+
bbox("FR", 41, 51.5, -5.5, 9.8),
|
|
122
|
+
bbox("GB", 49, 61, -8.7, 2),
|
|
123
|
+
bbox("HR", 42, 46.6, 13, 19.5),
|
|
124
|
+
bbox("IN", 6, 36, 68, 98),
|
|
125
|
+
bbox("NL", 50.7, 53.7, 3.3, 7.3),
|
|
126
|
+
bbox("NO", 57, 71.5, 4, 31),
|
|
127
|
+
bbox("PL", 49, 55, 14, 24.2),
|
|
128
|
+
bbox("PT", 36.5, 42.2, -9.6, -6.1),
|
|
129
|
+
bbox("RO", 43.5, 48.3, 20, 30),
|
|
130
|
+
bbox("SE", 55, 69.1, 10.9, 24.2),
|
|
131
|
+
bbox("SK", 47.7, 49.7, 16.8, 22.6),
|
|
132
|
+
bbox("SI", 45.4, 46.9, 13.3, 16.6),
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
export interface EmitCoverageManifestOptions {
|
|
136
|
+
/** The candidate DB under construction — MUST be pre-seal (a shipped DB is never patched, rebuild instead). */
|
|
137
|
+
dbPath: string
|
|
138
|
+
/** Coverage rows to bake (default {@link MEASURED_COUNTRY_COVERAGE}). */
|
|
139
|
+
coverage?: readonly CountryCoverageFact[]
|
|
140
|
+
/** Guard-B bbox rows to bake (default {@link MEASURED_COUNTRY_BBOXES}). */
|
|
141
|
+
bboxes?: readonly CountryBBoxFact[]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Bake the coverage manifest into a candidate DB under construction. Called by `buildCandidate` between the candidate
|
|
146
|
+
* build and the seal; standalone use is fine for tests/fixtures (never against a sealed artifact).
|
|
147
|
+
*/
|
|
148
|
+
export async function emitCoverageManifest(opts: EmitCoverageManifestOptions): Promise<void> {
|
|
149
|
+
const { writeGazetteerCoverageManifest } = await import("@mailwoman/resolver-wof-sqlite")
|
|
150
|
+
|
|
151
|
+
const db = new DatabaseSync(opts.dbPath)
|
|
152
|
+
const kdb = new DatabaseClient<GazetteerCoverageDatabase>({ database: db })
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
await writeGazetteerCoverageManifest(kdb, {
|
|
156
|
+
coverage: opts.coverage ?? MEASURED_COUNTRY_COVERAGE,
|
|
157
|
+
bboxes: opts.bboxes ?? MEASURED_COUNTRY_BBOXES,
|
|
158
|
+
})
|
|
159
|
+
} finally {
|
|
160
|
+
// `kdb` wraps the same handle; destroy() owns the close.
|
|
161
|
+
await kdb.destroy()
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Per-locale FST gazetteer build (`mailwoman gazetteer build fst`) — the decode-bias FST shipped as
|
|
7
|
+
* `fst-<locale>.bin` in the weights packages (#1318), rebuilt WITH degenerate-surface curation.
|
|
8
|
+
*
|
|
9
|
+
* THE CURATION (the "serialize runtime decisions into static indexes" doctrine): a name whose whole
|
|
10
|
+
* normalized surface is a bare function word ("la" — the case-folded Los Angeles alias colliding
|
|
11
|
+
* with the French article), a bare street-type word ("boulevard", "lane" — real US places that are
|
|
12
|
+
* street vocabulary everywhere else), or a composition of nothing but function words ("de la") is
|
|
13
|
+
* NEVER inserted as a bias key. This is the ASR-contextual-biasing "prune the bias list" discipline
|
|
14
|
+
* and Carmen's index-time token hygiene: the hazard is removed from the artifact rather than
|
|
15
|
+
* guarded at decode time, so it cannot misfire on lowercase, comma-free, any-locale input. The FST
|
|
16
|
+
* is a bias list, not the gazetteer of record — the resolver's candidate tables are untouched, so
|
|
17
|
+
* excluded places stay findable; they just stop nudging the decoder on degenerate keys.
|
|
18
|
+
*
|
|
19
|
+
* Exclusion sources are the SHIPPED libpostal dictionaries (`core/data/libpostal/dictionaries/`):
|
|
20
|
+
* per-language `stopwords.txt` (whole-surface + compositional clauses) and `street_types.txt`
|
|
21
|
+
* (whole-surface clause only — "Avenue Road" is a real name; "de la" is not). The language set is
|
|
22
|
+
* the served Latin-script tiers, uniform across locales (a FR query hits the en-us FST on the
|
|
23
|
+
* default path, so per-locale language scoping would under-curate).
|
|
24
|
+
*
|
|
25
|
+
* Provenance (policy string + excluded-insertion count) is recorded in the artifact trailer.
|
|
26
|
+
* Artifacts are written to --output (default: a `fst-per-locale-curated/` sibling of the shipped
|
|
27
|
+
* `fst-per-locale/` dir) — staged BESIDE, never overwriting; the swap into the shipped path is
|
|
28
|
+
* operator-gated after the battery.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
32
|
+
import { join, resolve } from "node:path"
|
|
33
|
+
|
|
34
|
+
import { dataRootPath, repoRootPathBuilder } from "@mailwoman/core/utils"
|
|
35
|
+
import { buildFSTFromWOF } from "@mailwoman/resolver-wof-sqlite/fst-builder"
|
|
36
|
+
import { normalizeTokens } from "@mailwoman/resolver-wof-sqlite/fst-matcher"
|
|
37
|
+
import { serializeFST } from "@mailwoman/resolver-wof-sqlite/fst-serialize"
|
|
38
|
+
|
|
39
|
+
/** The served Latin-script language tiers (see SCOPE.mdx) — uniform curation set for every locale FST. */
|
|
40
|
+
export const CURATION_LANGUAGES = [
|
|
41
|
+
"en",
|
|
42
|
+
"fr",
|
|
43
|
+
"de",
|
|
44
|
+
"nl",
|
|
45
|
+
"es",
|
|
46
|
+
"it",
|
|
47
|
+
"pt",
|
|
48
|
+
"da",
|
|
49
|
+
"nb",
|
|
50
|
+
"sv",
|
|
51
|
+
"fi",
|
|
52
|
+
"pl",
|
|
53
|
+
"cs",
|
|
54
|
+
"sk",
|
|
55
|
+
"sl",
|
|
56
|
+
"hr",
|
|
57
|
+
"hu",
|
|
58
|
+
] as const
|
|
59
|
+
|
|
60
|
+
export const EXCLUSION_POLICY_ID =
|
|
61
|
+
"degenerate-surface-exclusion v1.1 (libpostal stopwords+street_types, 17 langs, + supplemental)"
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Function-word surfaces the libpostal dictionaries MISS. Each entry carries its justification — this list is curated,
|
|
65
|
+
* not a dumping ground; a candidate belongs here only when it is a common function word in a served language whose
|
|
66
|
+
* libpostal stopword file lacks the bare form.
|
|
67
|
+
*/
|
|
68
|
+
export const SUPPLEMENTAL_DEGENERATE_SURFACES: ReadonlySet<string> = new Set([
|
|
69
|
+
// Dutch/Danish preposition ("op de hoek", "op til") — absent from libpostal nl/da stopwords.txt as
|
|
70
|
+
// a bare word. Shipped-index victim: the Overland Park "OP" initialism alias (wof 85945755).
|
|
71
|
+
"op",
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
/** The shipped per-locale FST set (provenance-recovered country scoping; en-nz deliberately has none). */
|
|
75
|
+
export const FST_LOCALES: ReadonlyMap<string, string[]> = new Map([
|
|
76
|
+
["en-us", ["US"]],
|
|
77
|
+
["fr-fr", ["FR"]],
|
|
78
|
+
["en-gb", ["GB"]],
|
|
79
|
+
["de-de", ["DE"]],
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
/** One dictionary line = canonical|variant|variant… — every pipe-separated form is a surface. */
|
|
83
|
+
function surfacesOfLine(line: string): string[] {
|
|
84
|
+
return line
|
|
85
|
+
.split("|")
|
|
86
|
+
.map((s) => s.trim())
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Load the degenerate-surface exclusion sets from the shipped libpostal dictionaries. Returns normalized-join keys
|
|
92
|
+
* (`normalizeTokens(surface).join(" ")`) so they compare exactly against the builder's insertion keys.
|
|
93
|
+
*/
|
|
94
|
+
export function loadDegenerateSurfaces(languages: readonly string[] = CURATION_LANGUAGES): {
|
|
95
|
+
surfaces: Set<string>
|
|
96
|
+
stopwordTokens: Set<string>
|
|
97
|
+
} {
|
|
98
|
+
const dictionariesDir = String(repoRootPathBuilder("core", "data", "libpostal", "dictionaries"))
|
|
99
|
+
const surfaces = new Set<string>()
|
|
100
|
+
const stopwordTokens = new Set<string>()
|
|
101
|
+
|
|
102
|
+
for (const lang of languages) {
|
|
103
|
+
for (const [file, isStopwords] of [
|
|
104
|
+
["stopwords.txt", true],
|
|
105
|
+
["street_types.txt", false],
|
|
106
|
+
] as const) {
|
|
107
|
+
const path = join(dictionariesDir, lang, file)
|
|
108
|
+
|
|
109
|
+
if (!existsSync(path)) continue
|
|
110
|
+
|
|
111
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
112
|
+
for (const surface of surfacesOfLine(line)) {
|
|
113
|
+
const tokens = normalizeTokens(surface)
|
|
114
|
+
|
|
115
|
+
if (tokens.length === 0) continue
|
|
116
|
+
surfaces.add(tokens.join(" "))
|
|
117
|
+
|
|
118
|
+
// Compositional clause sources from stopwords ONLY — single-token entries, so
|
|
119
|
+
// multi-word stopword phrases ("à côté de") never leak their content words in.
|
|
120
|
+
if (isStopwords && tokens.length === 1) {
|
|
121
|
+
stopwordTokens.add(tokens[0]!)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const s of SUPPLEMENTAL_DEGENERATE_SURFACES) {
|
|
129
|
+
const tokens = normalizeTokens(s)
|
|
130
|
+
|
|
131
|
+
if (tokens.length === 0) continue
|
|
132
|
+
surfaces.add(tokens.join(" "))
|
|
133
|
+
|
|
134
|
+
if (tokens.length === 1) {
|
|
135
|
+
stopwordTokens.add(tokens[0]!)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { surfaces, stopwordTokens }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface BuildLocaleFSTsOpts {
|
|
143
|
+
/** Locales to build (default: every FST_LOCALES key). */
|
|
144
|
+
locales?: string[]
|
|
145
|
+
/** WOF admin DB (default: `$MAILWOMAN_DATA_ROOT/wof/admin-global-priority.db`). */
|
|
146
|
+
dbPath?: string
|
|
147
|
+
/** Output dir (default: `$MAILWOMAN_DATA_ROOT/wof/fst-per-locale-curated`). Never the shipped dir. */
|
|
148
|
+
outputDir?: string
|
|
149
|
+
/** Skip the curation (an A/B control build with the SAME current DB). */
|
|
150
|
+
uncurated?: boolean
|
|
151
|
+
onProgress?: (line: string) => void
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface BuiltLocaleFST {
|
|
155
|
+
locale: string
|
|
156
|
+
path: string
|
|
157
|
+
bytes: number
|
|
158
|
+
nameInsertions: number
|
|
159
|
+
excludedInsertions: number
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function buildLocaleFSTs(opts: BuildLocaleFSTsOpts = {}): BuiltLocaleFST[] {
|
|
163
|
+
const locales = opts.locales ?? [...FST_LOCALES.keys()]
|
|
164
|
+
const dbPath = opts.dbPath ?? String(dataRootPath("wof", "admin-global-priority.db"))
|
|
165
|
+
const outputDir = resolve(opts.outputDir ?? String(dataRootPath("wof", "fst-per-locale-curated")))
|
|
166
|
+
const progress = opts.onProgress ?? (() => {})
|
|
167
|
+
|
|
168
|
+
const exclusion = opts.uncurated ? undefined : loadDegenerateSurfaces()
|
|
169
|
+
|
|
170
|
+
if (exclusion) {
|
|
171
|
+
progress(
|
|
172
|
+
`curation: ${exclusion.surfaces.size} whole surfaces + ${exclusion.stopwordTokens.size} stopword tokens (${EXCLUSION_POLICY_ID})`
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
mkdirSync(outputDir, { recursive: true })
|
|
176
|
+
|
|
177
|
+
const built: BuiltLocaleFST[] = []
|
|
178
|
+
|
|
179
|
+
for (const locale of locales) {
|
|
180
|
+
const countries = FST_LOCALES.get(locale)
|
|
181
|
+
|
|
182
|
+
if (!countries) throw new Error(`unknown FST locale ${locale} — add it to FST_LOCALES with its country scope`)
|
|
183
|
+
|
|
184
|
+
progress(`building fst-${locale} (countries=[${countries}]) from ${dbPath}`)
|
|
185
|
+
const { matcher, provenance } = buildFSTFromWOF({
|
|
186
|
+
dbPath,
|
|
187
|
+
countries,
|
|
188
|
+
...(exclusion
|
|
189
|
+
? {
|
|
190
|
+
excludeSurfaces: exclusion.surfaces,
|
|
191
|
+
excludeAllTokensOf: exclusion.stopwordTokens,
|
|
192
|
+
exclusionPolicy: EXCLUSION_POLICY_ID,
|
|
193
|
+
}
|
|
194
|
+
: {}),
|
|
195
|
+
onProgress: (phase, detail) => progress(` [${phase}] ${detail ?? ""}`),
|
|
196
|
+
})
|
|
197
|
+
const outPath = join(outputDir, `fst-${locale}${opts.uncurated ? ".uncurated" : ""}.bin`)
|
|
198
|
+
const bytes = serializeFST(matcher, provenance)
|
|
199
|
+
writeFileSync(outPath, bytes)
|
|
200
|
+
built.push({
|
|
201
|
+
locale,
|
|
202
|
+
path: outPath,
|
|
203
|
+
bytes: bytes.length,
|
|
204
|
+
nameInsertions: provenance.nameInsertions,
|
|
205
|
+
excludedInsertions: provenance.excludedInsertions ?? 0,
|
|
206
|
+
})
|
|
207
|
+
progress(
|
|
208
|
+
` wrote ${outPath} (${(bytes.length / 1e6).toFixed(1)} MB, ${provenance.nameInsertions} insertions, ${provenance.excludedInsertions ?? 0} excluded)`
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return built
|
|
213
|
+
}
|
|
@@ -40,6 +40,7 @@ import type { GeonamesIngestProgress } from "@mailwoman/resolver-wof-sqlite"
|
|
|
40
40
|
import type { BuildCandidateResult } from "@mailwoman/resolver-wof-sqlite/build-candidate"
|
|
41
41
|
|
|
42
42
|
import { mailwomanDataRoot } from "../resolver-backend.ts"
|
|
43
|
+
import { emitCoverageManifest } from "./coverage-manifest.ts"
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* The bilingual / alt-name EU set the GeoNames fold lifts (FI hard-resolve 69.5 → 85.8 %). GeoNames `<CC>.txt` dumps
|
|
@@ -190,7 +191,8 @@ export interface BuildOptions {
|
|
|
190
191
|
|
|
191
192
|
/**
|
|
192
193
|
* Build the byte-range candidate gazetteer from an admin DB + postcode shards. The FTS5-trigram fuzzy index is baked in
|
|
193
|
-
* by `buildCandidateTable
|
|
194
|
+
* by `buildCandidateTable`; the coverage manifest (survey candidate #2 — the artifact's own hard-filter coverage record
|
|
195
|
+
* + guard-B bboxes, see `coverage-manifest.ts`) is baked in before the seal.
|
|
194
196
|
*/
|
|
195
197
|
export async function buildCandidate(opts: BuildOptions): Promise<BuildCandidateResult> {
|
|
196
198
|
const { buildCandidateTable } = await import("@mailwoman/resolver-wof-sqlite/build-candidate")
|
|
@@ -201,6 +203,11 @@ export async function buildCandidate(opts: BuildOptions): Promise<BuildCandidate
|
|
|
201
203
|
postcodes: [...(opts.postcodeShards ?? resolvePostcodeShards())],
|
|
202
204
|
onProgress: opts.onProgress,
|
|
203
205
|
})
|
|
206
|
+
// Coverage manifest (survey candidate #2): facts about the artifact live IN the artifact — bake the
|
|
207
|
+
// measured hard-filter coverage record + guard-B bboxes so consumers read them at open instead of
|
|
208
|
+
// falling back to the code constants. MUST run pre-seal (a shipped DB is never patched — rebuild).
|
|
209
|
+
opts.onProgress?.("coverage-manifest", "baking country coverage + bbox manifest")
|
|
210
|
+
await emitCoverageManifest({ dbPath: opts.out })
|
|
204
211
|
// The sealed-artifact invariant: a built DB is a read-only asset from the moment it exists.
|
|
205
212
|
sealDatabase(opts.out)
|
|
206
213
|
|
|
@@ -314,6 +321,7 @@ export function defaultGazetteerVersion(now: Date, suffix = "a"): string {
|
|
|
314
321
|
|
|
315
322
|
return `${y}-${m}-${d}${suffix}`
|
|
316
323
|
}
|
|
324
|
+
export * from "./coverage-manifest.ts"
|
|
317
325
|
export * from "./defaults.ts"
|
|
318
326
|
export * from "./fts.ts"
|
|
319
327
|
export * from "./verify.ts"
|