mailwoman 7.2.1 → 7.4.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/commands/eval/poi-board.tsx +93 -0
- package/commands/gazetteer/build/poi-brands.tsx +91 -0
- package/commands/parse.tsx +8 -1
- package/commands/poi.tsx +16 -2
- package/default-reverse-geocoder.ts +64 -0
- package/eval-harness/poi-board.ts +563 -0
- package/gazetteer-pipeline/poi/build-brands.ts +243 -0
- package/gazetteer-pipeline/poi/build-poi.ts +3 -1
- package/out/commands/eval/poi-board.d.ts +32 -0
- package/out/commands/eval/poi-board.d.ts.map +1 -0
- package/out/commands/eval/poi-board.js +80 -0
- package/out/commands/eval/poi-board.js.map +1 -0
- package/out/commands/gazetteer/build/poi-brands.d.ts +27 -0
- package/out/commands/gazetteer/build/poi-brands.d.ts.map +1 -0
- package/out/commands/gazetteer/build/poi-brands.js +62 -0
- package/out/commands/gazetteer/build/poi-brands.js.map +1 -0
- package/out/commands/parse.d.ts.map +1 -1
- package/out/commands/parse.js +6 -1
- package/out/commands/parse.js.map +1 -1
- package/out/commands/poi.d.ts.map +1 -1
- package/out/commands/poi.js +14 -1
- package/out/commands/poi.js.map +1 -1
- package/out/default-reverse-geocoder.d.ts +35 -0
- package/out/default-reverse-geocoder.d.ts.map +1 -0
- package/out/default-reverse-geocoder.js +58 -0
- package/out/default-reverse-geocoder.js.map +1 -0
- package/out/eval-harness/poi-board.d.ts +192 -0
- package/out/eval-harness/poi-board.d.ts.map +1 -0
- package/out/eval-harness/poi-board.js +362 -0
- package/out/eval-harness/poi-board.js.map +1 -0
- package/out/gazetteer-pipeline/poi/build-brands.d.ts +119 -0
- package/out/gazetteer-pipeline/poi/build-brands.d.ts.map +1 -0
- package/out/gazetteer-pipeline/poi/build-brands.js +166 -0
- package/out/gazetteer-pipeline/poi/build-brands.js.map +1 -0
- package/out/gazetteer-pipeline/poi/build-poi.d.ts.map +1 -1
- package/out/gazetteer-pipeline/poi/build-poi.js +3 -2
- package/out/gazetteer-pipeline/poi/build-poi.js.map +1 -1
- package/out/poi-executor.d.ts +23 -0
- package/out/poi-executor.d.ts.map +1 -1
- package/out/poi-executor.js +42 -8
- package/out/poi-executor.js.map +1 -1
- package/out/poi-intent.d.ts +10 -1
- package/out/poi-intent.d.ts.map +1 -1
- package/out/poi-intent.js +60 -12
- package/out/poi-intent.js.map +1 -1
- package/out/runtime-pipeline.d.ts +11 -7
- package/out/runtime-pipeline.d.ts.map +1 -1
- package/out/runtime-pipeline.js +54 -14
- package/out/runtime-pipeline.js.map +1 -1
- package/package.json +23 -22
- package/poi-executor.ts +70 -10
- package/poi-intent.ts +72 -14
- package/runtime-pipeline.ts +77 -22
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `mailwoman eval poi-board` — the curated POI query board (spec §3.6, exotic-POI arc). Runs the
|
|
7
|
+
* real `createRuntimePipeline({ poiQueryKind: { poiDatabasePath } })` surface against every
|
|
8
|
+
* committed fixture and grades the ASSEMBLED answer (matched category + coordinate), not label F1.
|
|
9
|
+
*
|
|
10
|
+
* Floors (spec §3.6, set off the v1 baseline): `overall ≥ 90%`, `abstain = 100%`, `address = 100%`.
|
|
11
|
+
* They are graded and printed on EVERY run. Pass `--enforce` to turn a breach into a non-zero exit
|
|
12
|
+
* (the CI-gate mode). Without `--enforce` the command stays report-only — it exits 0 on case
|
|
13
|
+
* failures, and a non-zero exit means the HARNESS broke (missing fixtures, missing db, a pipeline
|
|
14
|
+
* construction error), never a graded case failing.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Text } from "ink"
|
|
18
|
+
import zod from "zod"
|
|
19
|
+
|
|
20
|
+
import { type CommandComponent, useCommandTask } from "../../cli-kit/index.ts"
|
|
21
|
+
import { runPoiBoard } from "../../eval-harness/poi-board.ts"
|
|
22
|
+
|
|
23
|
+
export const description = "POI query board (spec §3.6) — graded on the assembled answer, v1 report-only"
|
|
24
|
+
|
|
25
|
+
const OptionsSchema = zod.object({
|
|
26
|
+
locale: zod
|
|
27
|
+
.string()
|
|
28
|
+
.optional()
|
|
29
|
+
.default("en-US")
|
|
30
|
+
.describe("Weights package locale for the classifier (default en-US)"),
|
|
31
|
+
weightsCache: zod
|
|
32
|
+
.string()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Package-shaped candidate weights dir (mirrors eval parity --weights-cache)"),
|
|
35
|
+
fixtures: zod.string().optional().describe("Fixture JSONL override (default: the committed poi-board fixtures)"),
|
|
36
|
+
db: zod
|
|
37
|
+
.string()
|
|
38
|
+
.optional()
|
|
39
|
+
.describe("Sealed poi.db to query (default <data-root>/poi/poi.db — the gazetteer build poi default)"),
|
|
40
|
+
resolveDb: zod
|
|
41
|
+
.string()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe("WOF admin shard path(s) for anchor resolution, comma-separated (same as `mailwoman poi --resolve-db`)"),
|
|
44
|
+
candidateDb: zod
|
|
45
|
+
.string()
|
|
46
|
+
.optional()
|
|
47
|
+
.describe(
|
|
48
|
+
"Byte-range candidate.db for anchor resolution (same as `mailwoman poi --candidate-db`; wins over --resolve-db)"
|
|
49
|
+
),
|
|
50
|
+
json: zod
|
|
51
|
+
.boolean()
|
|
52
|
+
.optional()
|
|
53
|
+
.default(false)
|
|
54
|
+
.describe("Print the full report as JSON instead of the human-readable table"),
|
|
55
|
+
enforce: zod
|
|
56
|
+
.boolean()
|
|
57
|
+
.optional()
|
|
58
|
+
.default(false)
|
|
59
|
+
.describe("Exit non-zero if any pre-registered floor is breached (overall ≥ 90%, abstain/address = 100%)"),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
export { OptionsSchema as options }
|
|
63
|
+
|
|
64
|
+
const EvalPoiBoard: CommandComponent<typeof OptionsSchema> = ({ options }) => {
|
|
65
|
+
const state = useCommandTask(
|
|
66
|
+
async () => {
|
|
67
|
+
const { report, exitCode } = await runPoiBoard({
|
|
68
|
+
locale: options.locale,
|
|
69
|
+
weightsCacheRoot: options.weightsCache,
|
|
70
|
+
fixturesPath: options.fixtures,
|
|
71
|
+
db: options.db,
|
|
72
|
+
resolveDb: options.resolveDb,
|
|
73
|
+
candidateDb: options.candidateDb,
|
|
74
|
+
quiet: options.json,
|
|
75
|
+
enforce: options.enforce,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
return { report, exitCode }
|
|
79
|
+
},
|
|
80
|
+
({ exitCode }) => exitCode
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if (state.status === "error") return <Text color="red">✗ {state.message}</Text>
|
|
84
|
+
|
|
85
|
+
if (options.json && state.status === "done") {
|
|
86
|
+
return <Text>{JSON.stringify(state.result.report, null, 2)}</Text>
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Non-json mode: the runner narrates its table on stdout directly.
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default EvalPoiBoard
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `mailwoman gazetteer build poi-brands` — the POI brand lexicon builder, part 1 of 2 (part 2 wires
|
|
7
|
+
* `lookupPOIBrand` into the runtime pipeline; no pipeline wiring here). Thin wiring only: the read +
|
|
8
|
+
* aggregate + write logic lives in `gazetteer-pipeline/poi/build-brands.ts`, mirroring `build/poi.tsx`'s
|
|
9
|
+
* thin-command style. Reads a BUILT `poi.db` READ-ONLY — never writes one.
|
|
10
|
+
*
|
|
11
|
+
* `writeBrandTable`'s plain `JSON.stringify` doesn't collapse short primitive arrays onto one line the
|
|
12
|
+
* way `oxfmt` does (e.g. `"aliases": ["Foo"]` vs a 3-line array) — the process-y bit (shelling out, like
|
|
13
|
+
* `poi.tsx`'s own `git rev-parse`) belongs at the command layer, not in the pure/testable builder. Runs
|
|
14
|
+
* `oxfmt` on the output here so the emitted file is commit-ready without a manual format pass.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { execFileSync } from "node:child_process"
|
|
18
|
+
|
|
19
|
+
import { Box, Text } from "ink"
|
|
20
|
+
import zod from "zod"
|
|
21
|
+
|
|
22
|
+
import { type CommandComponent, useCommandTask } from "../../../cli-kit/index.ts"
|
|
23
|
+
import {
|
|
24
|
+
buildBrandTable,
|
|
25
|
+
DEFAULT_DOMINANCE,
|
|
26
|
+
DEFAULT_MIN_ROWS,
|
|
27
|
+
defaultBrandTableOutPath,
|
|
28
|
+
defaultPOIDatabasePath,
|
|
29
|
+
writeBrandTable,
|
|
30
|
+
} from "../../../gazetteer-pipeline/poi/build-brands.ts"
|
|
31
|
+
|
|
32
|
+
const OptionsSchema = zod.object({
|
|
33
|
+
db: zod.string().optional().describe("Built poi.db to read. Default <data-root>/poi/poi.db"),
|
|
34
|
+
out: zod.string().optional().describe("brands.json output path. Default poi-taxonomy/data/brands.json"),
|
|
35
|
+
minRows: zod.string().optional().describe(`Minimum total rows to keep a brand. Default ${DEFAULT_MIN_ROWS}`),
|
|
36
|
+
dominance: zod
|
|
37
|
+
.string()
|
|
38
|
+
.optional()
|
|
39
|
+
.describe(
|
|
40
|
+
`Minimum fraction of a QID's total rows its modal name must cover to qualify — below this the QID is ` +
|
|
41
|
+
`dropped entirely (systematic mistagging). Default ${DEFAULT_DOMINANCE}`
|
|
42
|
+
),
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
export { OptionsSchema as options }
|
|
46
|
+
|
|
47
|
+
const GazetteerBuildPOIBrands: CommandComponent<typeof OptionsSchema> = ({ options }) => {
|
|
48
|
+
const state = useCommandTask(async () => {
|
|
49
|
+
const dbPath = options.db ?? defaultPOIDatabasePath()
|
|
50
|
+
const out = options.out ?? defaultBrandTableOutPath()
|
|
51
|
+
const minRows = options.minRows ? Number.parseInt(options.minRows, 10) : DEFAULT_MIN_ROWS
|
|
52
|
+
const dominance = options.dominance ? Number.parseFloat(options.dominance) : DEFAULT_DOMINANCE
|
|
53
|
+
|
|
54
|
+
console.error(`▸ reading ${dbPath}`)
|
|
55
|
+
const table = await buildBrandTable({ dbPath, minRows, dominance })
|
|
56
|
+
|
|
57
|
+
console.error(`▸ writing ${out}`)
|
|
58
|
+
writeBrandTable(table, out)
|
|
59
|
+
execFileSync("yarn", ["oxfmt", out])
|
|
60
|
+
|
|
61
|
+
const top5 = table.brands
|
|
62
|
+
.slice(0, 5)
|
|
63
|
+
.map((b, i) => ` ${i + 1}. ${b.name} (${b.wikidata}) — ${b.rows.toLocaleString()} rows`)
|
|
64
|
+
|
|
65
|
+
return [
|
|
66
|
+
`brands.json: ${out} (${table.brands.length.toLocaleString()} brands, min-rows=${minRows}, dominance=${dominance})`,
|
|
67
|
+
`source: ${table.sourceLayer.name} ${table.sourceLayer.version} (vintage ${table.sourceLayer.sourceVintage})`,
|
|
68
|
+
"top 5 by rows:",
|
|
69
|
+
...top5,
|
|
70
|
+
]
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
if (state.status === "error") return <Text color="red">✗ {state.message}</Text>
|
|
74
|
+
|
|
75
|
+
if (state.status === "done") {
|
|
76
|
+
return (
|
|
77
|
+
<Box flexDirection="column">
|
|
78
|
+
{state.result.map((line, i) => (
|
|
79
|
+
<Text key={i} color={i === 0 ? "green" : undefined}>
|
|
80
|
+
{i === 0 ? "✓ " : " "}
|
|
81
|
+
{line}
|
|
82
|
+
</Text>
|
|
83
|
+
))}
|
|
84
|
+
</Box>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return null // progress streams to stderr until the summary lands
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default GazetteerBuildPOIBrands
|
package/commands/parse.tsx
CHANGED
|
@@ -62,7 +62,14 @@ const ParseConfigSchema = zod.object({
|
|
|
62
62
|
.optional()
|
|
63
63
|
.default(false)
|
|
64
64
|
.describe("In pipeline mode, skip the neural classifier (run normalize + queryShape + kind + resolver only)."),
|
|
65
|
-
poi: zod
|
|
65
|
+
poi: zod
|
|
66
|
+
.boolean()
|
|
67
|
+
.optional()
|
|
68
|
+
.default(true)
|
|
69
|
+
.describe(
|
|
70
|
+
"poi_query detection (poiQueryKind flag). DEFAULT-ON since 2026-07-20 (promotion battery: 0/4507 golden " +
|
|
71
|
+
"misroutes, 6/6 demo presets byte-identical). Pass --no-poi to restore the pre-flag address-only kind classification."
|
|
72
|
+
),
|
|
66
73
|
downloadWeights: zod
|
|
67
74
|
.boolean()
|
|
68
75
|
.optional()
|
package/commands/poi.tsx
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
import { existsSync } from "node:fs"
|
|
29
29
|
|
|
30
30
|
import { Spinner } from "@inkjs/ui"
|
|
31
|
-
import type { POIIntent, POIIntentOutcome } from "@mailwoman/core/pipeline"
|
|
31
|
+
import type { POIIntent, POIIntentOutcome, POIResult } from "@mailwoman/core/pipeline"
|
|
32
32
|
import { NeuralAddressClassifier } from "@mailwoman/neural"
|
|
33
33
|
import { getPOICategory } from "@mailwoman/poi-taxonomy"
|
|
34
34
|
import { createWOFResolver, type Resolver } from "@mailwoman/resolver"
|
|
@@ -168,6 +168,20 @@ function formatOverpassBlock(intent: POIIntent): string {
|
|
|
168
168
|
return emitOverpassQL(intent)
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Compact ancestry suffix, e.g. "· Springfield, IL, US" — locality/region/country entries, coarsest-last (the
|
|
173
|
+
* hierarchy's own deepest-first order reversed), skipping other placetypes (county, neighbourhood, …) to keep the table
|
|
174
|
+
* narrow. Empty string when `ancestry` is absent (no reverse geocoder wired) or carries none of those three tiers (e.g.
|
|
175
|
+
* open-ocean/approximate misses).
|
|
176
|
+
*/
|
|
177
|
+
function formatAncestrySuffix(ancestry: POIResult["ancestry"]): string {
|
|
178
|
+
if (!ancestry || ancestry.length === 0) return ""
|
|
179
|
+
const byPlacetype = new Map(ancestry.map((a) => [a.placetype, a.name]))
|
|
180
|
+
const parts = ["locality", "region", "country"].map((t) => byPlacetype.get(t)).filter((name) => name !== undefined)
|
|
181
|
+
|
|
182
|
+
return parts.length > 0 ? ` · ${parts.join(", ")}` : ""
|
|
183
|
+
}
|
|
184
|
+
|
|
171
185
|
function formatResultsTable(results: NonNullable<Extract<POIIntentOutcome, { type: "intent" }>["results"]>): string[] {
|
|
172
186
|
if (results.length === 0) return ["(no results)"]
|
|
173
187
|
|
|
@@ -184,7 +198,7 @@ function formatResultsTable(results: NonNullable<Extract<POIIntentOutcome, { typ
|
|
|
184
198
|
(r.distanceM !== undefined ? String(Math.round(r.distanceM)) : "-").padStart(10),
|
|
185
199
|
r.latitude.toFixed(6).padStart(12),
|
|
186
200
|
r.longitude.toFixed(6).padStart(12),
|
|
187
|
-
].join(" ")
|
|
201
|
+
].join(" ") + formatAncestrySuffix(r.ancestry)
|
|
188
202
|
)
|
|
189
203
|
}
|
|
190
204
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* The default read-time WOF reverse geocoder for POI ancestry — the poiQueryKind register row's second
|
|
7
|
+
* debt payment (runtime-flags.mdx): `createRuntimePipeline({ poiQueryKind: { poiDatabasePath } })`
|
|
8
|
+
* lazily loads this once per process and wires a SYNCHRONOUS adapter into `createPOIExecutor`'s
|
|
9
|
+
* `reverseGeocode` dep (the executor's return type carries no `Promise` — see `poi-intent.ts`'s
|
|
10
|
+
* `deps.execute`, called synchronously). `WOFReverseGeocoder.reverseGeocodeSync` (added alongside this
|
|
11
|
+
* feature) is the real synchronous core the async `reverseGeocode` already wrapped — see
|
|
12
|
+
* resolver-wof-sqlite/reverse.ts.
|
|
13
|
+
*
|
|
14
|
+
* `@mailwoman/resolver-wof-sqlite` is an optional peer dep and the admin gazetteer (with its
|
|
15
|
+
* `place_bbox` R*Tree, `mailwoman gazetteer build fts`) is a multi-GB build artifact that may not be on
|
|
16
|
+
* disk — either gap yields `null` and POI results simply carry no `ancestry` key at all (house
|
|
17
|
+
* meaning-of-zero: absence, never an empty array). The polygon sidecar (`wof-polygons.db`) is OPTIONAL
|
|
18
|
+
* too; without it every ancestry chain still resolves, just `containment: "approximate"`
|
|
19
|
+
* (`WOFReverseGeocoder`'s own centroid-descent fallback).
|
|
20
|
+
*
|
|
21
|
+
* Admin DB resolution mirrors `resolver-backend.ts`'s `wofShardPaths()` — the SAME default
|
|
22
|
+
* `@mailwoman/photon`'s and `@mailwoman/nominatim`'s `serve` commands use for their own
|
|
23
|
+
* `WOFReverseGeocoder` (`photon/cli.ts`, `nominatim/cli.ts`): first existing shard in the list wins
|
|
24
|
+
* (`admin-global-priority.db` first). The polygon sidecar is read from `$MAILWOMAN_WOF_POLYGONS_DB` —
|
|
25
|
+
* the same env var `mailwoman reverse` reads (`commands/reverse.tsx`).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { existsSync } from "node:fs"
|
|
29
|
+
|
|
30
|
+
import { $public } from "@mailwoman/core/env"
|
|
31
|
+
import type { WOFReverseGeocoder as WOFReverseGeocoderType } from "@mailwoman/resolver-wof-sqlite"
|
|
32
|
+
|
|
33
|
+
import { wofShardPaths } from "./resolver-backend.ts"
|
|
34
|
+
|
|
35
|
+
let cached: Promise<WOFReverseGeocoderType | null> | null = null
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Lazy-load + cache the default `WOFReverseGeocoder`. `null` when no admin shard with a `place_bbox` R*Tree is on disk,
|
|
39
|
+
* or `@mailwoman/resolver-wof-sqlite` can't be resolved (stripped install) — the pipeline then wires no
|
|
40
|
+
* `reverseGeocode` fn at all, so POI results degrade to no `ancestry` (byte-stable pre-feature behavior). Cached for
|
|
41
|
+
* the process lifetime (one handle, reused).
|
|
42
|
+
*/
|
|
43
|
+
export function loadDefaultReverseGeocoder(): Promise<WOFReverseGeocoderType | null> {
|
|
44
|
+
if (!cached) {
|
|
45
|
+
cached = (async (): Promise<WOFReverseGeocoderType | null> => {
|
|
46
|
+
try {
|
|
47
|
+
const adminDBPath = wofShardPaths().filter(existsSync)[0]
|
|
48
|
+
|
|
49
|
+
if (!adminDBPath) return null
|
|
50
|
+
const { WOFReverseGeocoder } = await import("@mailwoman/resolver-wof-sqlite")
|
|
51
|
+
const polygonDBPath = $public.MAILWOMAN_WOF_POLYGONS_DB
|
|
52
|
+
|
|
53
|
+
return new WOFReverseGeocoder({
|
|
54
|
+
adminDBPath,
|
|
55
|
+
...(polygonDBPath && existsSync(polygonDBPath) ? { polygonDBPath } : {}),
|
|
56
|
+
})
|
|
57
|
+
} catch {
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
})()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return cached
|
|
64
|
+
}
|