mailwoman 4.6.0 → 4.7.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geocode.d.ts","sourceRoot":"","sources":["../../commands/geocode.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAQH,OAAO,GAAG,MAAM,KAAK,CAAA;AAOrB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAOrD,QAAA,MAAM,eAAe,6BAA4E,CAAA;AACjG,OAAO,EAAE,eAAe,IAAI,IAAI,EAAE,aAAa,IAAI,OAAO,EAAE,CAAA;AAE5D,QAAA,MAAM,aAAa;;;;;;;;;;;;qBAyDjB,CAAA;AAkHF,QAAA,MAAM,cAAc,EAAE,gBAAgB,CAAC,OAAO,aAAa,EAAE,OAAO,eAAe,CAgClF,CAAA;AAED,eAAe,cAAc,CAAA"}
@@ -0,0 +1,214 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * @copyright Sister Software
4
+ * @license AGPL-3.0
5
+ * @author Teffen Ellis, et al.
6
+ *
7
+ * `mailwoman geocode "<address>" [flags]` — end-to-end street-level geocoder.
8
+ *
9
+ * Pipeline:
10
+ * 1. Parse the address with the neural classifier (same path as `parse` command).
11
+ * 2. Resolve admin hierarchy via `createWofResolver(WofSqlitePlaceLookup)`.
12
+ * 3. Augment with per-state address-point (situs) + interpolation shards, selected from
13
+ * the resolved region. Both are optional — absent shards degrade gracefully to admin-only.
14
+ * 4. Extract the best available coordinate + resolution tier from the resolved tree and emit
15
+ * a flat geocode result object.
16
+ *
17
+ * Resolution tiers (best → worst):
18
+ * - `address_point` — exact situs coordinate from the address-points shard
19
+ * - `interpolated` — house-number estimate from the interpolation shard
20
+ * - `admin` — admin centroid from the WOF gazetteer
21
+ *
22
+ * Exit-code contract:
23
+ * - 0 successful geocode (including admin-only degradation when shards are absent)
24
+ * - 1 bad arguments, missing required DB, or fatal parse/resolve error
25
+ */
26
+ import { Spinner } from "@inkjs/ui";
27
+ import { createWofResolver } from "@mailwoman/core/resolver";
28
+ import { NeuralAddressClassifier } from "@mailwoman/neural";
29
+ import { Text } from "ink";
30
+ import { setImmediate } from "node:timers/promises";
31
+ import { useEffect, useState } from "react";
32
+ import zod from "zod";
33
+ import { geocodeAddress, ShardProvider, } from "../geocode-core.js";
34
+ import { resolverDefaultCountry } from "./parse.js";
35
+ // ---------------------------------------------------------------------------
36
+ // CLI contract — args + options
37
+ // ---------------------------------------------------------------------------
38
+ const ArgumentsSchema = zod.array(zod.string().describe("A formatted postal address to geocode"));
39
+ export { ArgumentsSchema as args, OptionsSchema as options };
40
+ const OptionsSchema = zod.object({
41
+ locale: zod
42
+ .string()
43
+ .regex(/^[a-z]{2}(-[A-Z]{2})?$/u, "Expected a BCP-47 tag like en-US or fr-FR")
44
+ .optional()
45
+ .default("en-US")
46
+ .describe("Locale tag matching a weights package (en-US, fr-FR). Default en-US."),
47
+ defaultCountry: zod
48
+ .string()
49
+ .optional()
50
+ .describe("ISO-3166 country to scope the WOF resolver. Defaults from --locale's region subtag (en-US → US). " +
51
+ "Pass 'none' to disable the country filter."),
52
+ resolveDb: zod
53
+ .string()
54
+ .optional()
55
+ .describe("Path to a WOF admin SQLite distribution. Defaults to $MAILWOMAN_WOF_DB; errors if neither is set."),
56
+ dataRoot: zod
57
+ .string()
58
+ .optional()
59
+ .default("/mnt/playpen/mailwoman-data")
60
+ .describe("Root directory for per-state address-point and interpolation shards. " +
61
+ "Shards are expected at <dataRoot>/address-points/address-points-us-<state>.db " +
62
+ "and <dataRoot>/interpolation/interpolation-us-<state>.db. Default: /mnt/playpen/mailwoman-data."),
63
+ addressPointsDb: zod
64
+ .string()
65
+ .optional()
66
+ .describe("Explicit path to an address-points (situs) SQLite shard. Bypasses the per-state shard selection " +
67
+ "from the resolved region. Use when you already know the right shard or are testing a specific file."),
68
+ interpolationDb: zod
69
+ .string()
70
+ .optional()
71
+ .describe("Explicit path to an interpolation SQLite shard. Bypasses the per-state shard selection. " +
72
+ "Use when you already know the right shard or are testing a specific file."),
73
+ interpCalibration: zod
74
+ .number()
75
+ .optional()
76
+ .default(1.7)
77
+ .describe("Conformal calibration multiplier for the interpolation tier's reported uncertainty_m (#374). " +
78
+ "The raw half-segment radius covers only ~72% of true errors; the default 1.7 (Travis-County " +
79
+ "calibration, 2026-06-14) lifts that to a ~90% bound. Pass 1 to report the raw heuristic radius."),
80
+ format: zod
81
+ .enum(["json", "text"])
82
+ .optional()
83
+ .default("json")
84
+ .describe('Output format. "json" (default) emits a machine-readable object; "text" prints a human summary.'),
85
+ });
86
+ // ---------------------------------------------------------------------------
87
+ // Path helpers
88
+ // ---------------------------------------------------------------------------
89
+ function resolveWofPath(options) {
90
+ const path = options.resolveDb ?? process.env["MAILWOMAN_WOF_DB"];
91
+ if (!path) {
92
+ throw new Error("geocode needs a WOF admin SQLite path. Set $MAILWOMAN_WOF_DB or pass --resolve-db <path>. " +
93
+ "Build one with `mailwoman-wof-build-slim` + `mailwoman-wof-build-fts`.");
94
+ }
95
+ return path;
96
+ }
97
+ // ---------------------------------------------------------------------------
98
+ // Core geocode logic
99
+ // ---------------------------------------------------------------------------
100
+ async function runGeocode(input, options) {
101
+ // Resolve the WOF admin path FIRST — it's the most common missing prerequisite and the cheapest to
102
+ // check, so surface that error before the (slower) weights load. (Order matters for the CLI contract:
103
+ // a missing WOF DB must report the WOF error even when the weights are also absent.)
104
+ const wofPath = resolveWofPath(options);
105
+ // Load the neural classifier (required for street-level; weights must be present).
106
+ let classifier;
107
+ try {
108
+ classifier = await NeuralAddressClassifier.loadFromWeights({ locale: options.locale });
109
+ }
110
+ catch {
111
+ throw new Error("geocode requires the neural weights. Install @mailwoman/neural-weights-en-us (or pass --locale with installed weights).");
112
+ }
113
+ // Open the WOF admin resolver + the situs/interpolation shard provider.
114
+ let mod;
115
+ try {
116
+ mod = await import("@mailwoman/resolver-wof-sqlite");
117
+ }
118
+ catch {
119
+ throw new Error("geocode requires `@mailwoman/resolver-wof-sqlite` to be installed. " +
120
+ "Run `npm install @mailwoman/resolver-wof-sqlite` and try again.");
121
+ }
122
+ const lookup = new mod.WofSqlitePlaceLookup({ databasePath: wofPath });
123
+ const shardProvider = new ShardProvider(mod, options.dataRoot);
124
+ // Explicit --address-points-db / --interpolation-db flags override per-state selection (testing a
125
+ // specific file); an unset tier still falls back to the region-derived per-state shard.
126
+ const explicitAp = options.addressPointsDb ? new mod.AddressPointSqliteLookup(options.addressPointsDb) : undefined;
127
+ const explicitIp = options.interpolationDb ? new mod.StreetInterpolator({ dbPath: options.interpolationDb }) : undefined;
128
+ const shards = explicitAp || explicitIp
129
+ ? (slug) => {
130
+ const base = explicitAp && explicitIp ? {} : shardProvider.for(slug);
131
+ return { addressPoints: explicitAp ?? base.addressPoints, interpolation: explicitIp ?? base.interpolation };
132
+ }
133
+ : shardProvider.for;
134
+ try {
135
+ const resolver = createWofResolver(lookup);
136
+ const result = await geocodeAddress(input, {
137
+ classifier,
138
+ resolver,
139
+ shards,
140
+ defaultCountry: resolverDefaultCountry(options) || undefined,
141
+ interpCalibration: options.interpCalibration,
142
+ });
143
+ return options.format === "text" ? formatText(result) : JSON.stringify(result, null, 2);
144
+ }
145
+ finally {
146
+ explicitAp?.close();
147
+ explicitIp?.close();
148
+ shardProvider.close();
149
+ lookup.close();
150
+ }
151
+ }
152
+ // ---------------------------------------------------------------------------
153
+ // Text formatter
154
+ // ---------------------------------------------------------------------------
155
+ function formatText(result) {
156
+ const lines = [];
157
+ lines.push(`input: ${result.input}`);
158
+ lines.push(`resolution_tier: ${result.resolution_tier}`);
159
+ if (result.lat != null && result.lon != null) {
160
+ lines.push(`coordinate: ${result.lat.toFixed(6)}, ${result.lon.toFixed(6)}`);
161
+ }
162
+ else {
163
+ lines.push(`coordinate: (unresolved)`);
164
+ }
165
+ if (result.uncertainty_m != null) {
166
+ lines.push(`uncertainty_m: ${result.uncertainty_m}`);
167
+ }
168
+ if (result.locality)
169
+ lines.push(`locality: ${result.locality}`);
170
+ if (result.region)
171
+ lines.push(`region: ${result.region}`);
172
+ if (result.postcode)
173
+ lines.push(`postcode: ${result.postcode}`);
174
+ if (result.hierarchy.length > 0) {
175
+ lines.push("hierarchy:");
176
+ for (const h of result.hierarchy) {
177
+ const coord = h.lat != null ? ` (${h.lat.toFixed(4)}, ${h.lon.toFixed(4)})` : "";
178
+ const id = h.placeId ? ` [${h.placeId}]` : "";
179
+ lines.push(` ${h.tag.padEnd(20)} ${h.value}${id}${coord}`);
180
+ }
181
+ }
182
+ return lines.join("\n");
183
+ }
184
+ // ---------------------------------------------------------------------------
185
+ // React command component
186
+ // ---------------------------------------------------------------------------
187
+ const GeocodeCommand = ({ args, options }) => {
188
+ const [output, setOutput] = useState();
189
+ const [error, setError] = useState();
190
+ useEffect(() => {
191
+ if (error) {
192
+ setImmediate().then(() => process.exit(1));
193
+ }
194
+ }, [error]);
195
+ useEffect(() => {
196
+ const input = args[0];
197
+ if (!input || input.trim().length === 0) {
198
+ setError("geocode requires a positional address argument (e.g. mailwoman geocode \"350 5th Ave, New York, NY\")");
199
+ return;
200
+ }
201
+ runGeocode(input.trim(), options)
202
+ .then(setOutput)
203
+ .catch((err) => setError(err.message));
204
+ }, [args, options]);
205
+ if (error) {
206
+ return _jsx(Text, { color: "red", children: error });
207
+ }
208
+ if (!output) {
209
+ return _jsx(Spinner, {});
210
+ }
211
+ return _jsx(Text, { children: output });
212
+ };
213
+ export default GeocodeCommand;
214
+ //# sourceMappingURL=geocode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geocode.js","sourceRoot":"","sources":["../../commands/geocode.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reverse.d.ts","sourceRoot":"","sources":["../../commands/reverse.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,OAAO,GAAG,MAAM,KAAK,CAAA;AACrB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAErD,QAAA,MAAM,eAAe,6BAEuE,CAAA;AAE5F,QAAA,MAAM,aAAa;;;;;;;qBAqBjB,CAAA;AAEF,OAAO,EAAE,eAAe,IAAI,IAAI,EAAE,aAAa,IAAI,OAAO,EAAE,CAAA;AA+E5D,QAAA,MAAM,cAAc,EAAE,gBAAgB,CAAC,OAAO,aAAa,EAAE,OAAO,eAAe,CA6ClF,CAAA;AAED,eAAe,cAAc,CAAA"}
@@ -0,0 +1,143 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * @copyright Sister Software
4
+ * @license AGPL-3.0
5
+ * @author Teffen Ellis, et al.
6
+ *
7
+ * `mailwoman reverse <lat> <lon>` — resolve a WGS-84 coordinate to its containing admin hierarchy
8
+ * via the WofReverseGeocoder (#484).
9
+ *
10
+ * Falls back to the MAILWOMAN_WOF_ADMIN_DB / MAILWOMAN_WOF_POLYGONS_DB env vars when the
11
+ * corresponding flags are absent. Prints a JSON object with the resolved hierarchy and the
12
+ * containment kind (polygon | approximate) so callers can gauge result quality.
13
+ *
14
+ * Exit-code contract:
15
+ * - 0 successful reverse-geocode (including "open ocean" empty-hierarchy)
16
+ * - 1 bad arguments or DB path missing / wrong
17
+ */
18
+ import { Spinner } from "@inkjs/ui";
19
+ import { Text } from "ink";
20
+ import { setImmediate } from "node:timers/promises";
21
+ import { useEffect, useState } from "react";
22
+ import zod from "zod";
23
+ const ArgumentsSchema = zod
24
+ .array(zod.string())
25
+ .describe("Positional args: <lat> <lon> — WGS-84 decimal degrees (e.g. 40.7128 -74.0060).");
26
+ const OptionsSchema = zod.object({
27
+ adminDb: zod
28
+ .string()
29
+ .optional()
30
+ .describe("Path to the admin gazetteer SQLite DB (must carry the package-built place_bbox R*Tree). " +
31
+ "Defaults to $MAILWOMAN_WOF_ADMIN_DB."),
32
+ polygonsDb: zod
33
+ .string()
34
+ .optional()
35
+ .describe("Path to the polygon sidecar DB (wof-polygons.db, table polygons(id, geom)). " +
36
+ "Without it every result is containment: approximate. " +
37
+ "Defaults to $MAILWOMAN_WOF_POLYGONS_DB."),
38
+ format: zod
39
+ .enum(["json", "text"])
40
+ .optional()
41
+ .default("json")
42
+ .describe('Output format. "json" emits a machine-readable object; "text" prints a human-readable hierarchy.'),
43
+ });
44
+ export { ArgumentsSchema as args, OptionsSchema as options };
45
+ function resolveAdminDbPath(options) {
46
+ const path = options.adminDb ?? process.env["MAILWOMAN_WOF_ADMIN_DB"];
47
+ if (!path) {
48
+ throw new Error("reverse needs an admin DB path. Set $MAILWOMAN_WOF_ADMIN_DB or pass --admin-db <path>. " +
49
+ "Build one with `mailwoman-wof-build-fts <path-to-wof.db>` after building the WOF SQLite " +
50
+ "distribution with `mailwoman-wof-build-slim`.");
51
+ }
52
+ return path;
53
+ }
54
+ function resolvePolygonsDbPath(options) {
55
+ return options.polygonsDb ?? process.env["MAILWOMAN_WOF_POLYGONS_DB"];
56
+ }
57
+ async function runReverse(lat, lon, options) {
58
+ let mod;
59
+ try {
60
+ mod = await import("@mailwoman/resolver-wof-sqlite");
61
+ }
62
+ catch {
63
+ throw new Error("reverse requires `@mailwoman/resolver-wof-sqlite` to be installed. " +
64
+ "Run `npm install @mailwoman/resolver-wof-sqlite` and try again.");
65
+ }
66
+ const adminDbPath = resolveAdminDbPath(options);
67
+ const polygonDbPath = resolvePolygonsDbPath(options);
68
+ const geocoder = new mod.WofReverseGeocoder({ adminDbPath, polygonDbPath });
69
+ try {
70
+ const result = await geocoder.reverseGeocode(lat, lon);
71
+ if (options.format === "text") {
72
+ const lines = [];
73
+ lines.push(`containment: ${result.containment}`);
74
+ if (result.hierarchy.length === 0) {
75
+ lines.push("(no admin hierarchy — point may be in open ocean or outside the gazetteer coverage)");
76
+ }
77
+ else {
78
+ for (const place of result.hierarchy) {
79
+ const distStr = place.distanceKm !== undefined ? ` (~${place.distanceKm.toFixed(1)} km from centroid)` : "";
80
+ lines.push(` ${place.placetype.padEnd(16)} ${place.name} [wof:${place.id}]${distStr}`);
81
+ }
82
+ }
83
+ return lines.join("\n");
84
+ }
85
+ // JSON: emit a tidy object — the full hierarchy array + containment at the top level.
86
+ return JSON.stringify({
87
+ lat,
88
+ lon,
89
+ containment: result.containment,
90
+ hierarchy: result.hierarchy.map((p) => ({
91
+ id: p.id,
92
+ name: p.name,
93
+ placetype: p.placetype,
94
+ country: p.country,
95
+ lat: p.lat,
96
+ lon: p.lon,
97
+ ...(p.distanceKm !== undefined ? { distanceKm: p.distanceKm } : {}),
98
+ })),
99
+ }, null, 2);
100
+ }
101
+ finally {
102
+ geocoder.close();
103
+ }
104
+ }
105
+ const ReverseCommand = ({ args, options }) => {
106
+ const [output, setOutput] = useState();
107
+ const [error, setError] = useState();
108
+ useEffect(() => {
109
+ if (error) {
110
+ setImmediate().then(() => process.exit(1));
111
+ }
112
+ }, [error]);
113
+ useEffect(() => {
114
+ const rawLat = args[0];
115
+ const rawLon = args[1];
116
+ if (!rawLat || !rawLon) {
117
+ setError("reverse requires two positional arguments: <lat> <lon> (e.g. mailwoman reverse 40.7128 -74.0060)");
118
+ return;
119
+ }
120
+ const lat = Number(rawLat);
121
+ const lon = Number(rawLon);
122
+ if (!Number.isFinite(lat) || Math.abs(lat) > 90) {
123
+ setError(`Invalid latitude ${JSON.stringify(rawLat)} — must be a number in [-90, 90].`);
124
+ return;
125
+ }
126
+ if (!Number.isFinite(lon) || Math.abs(lon) > 180) {
127
+ setError(`Invalid longitude ${JSON.stringify(rawLon)} — must be a number in [-180, 180].`);
128
+ return;
129
+ }
130
+ runReverse(lat, lon, options)
131
+ .then(setOutput)
132
+ .catch((err) => setError(err.message));
133
+ }, [args, options]);
134
+ if (error) {
135
+ return _jsx(Text, { color: "red", children: error });
136
+ }
137
+ if (!output) {
138
+ return _jsx(Spinner, {});
139
+ }
140
+ return _jsx(Text, { children: output });
141
+ };
142
+ export default ReverseCommand;
143
+ //# sourceMappingURL=reverse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reverse.js","sourceRoot":"","sources":["../../commands/reverse.tsx"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,CAAA;AAC1B,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAC3C,OAAO,GAAG,MAAM,KAAK,CAAA;AAGrB,MAAM,eAAe,GAAG,GAAG;KACzB,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;KACnB,QAAQ,CAAC,gFAAgF,CAAC,CAAA;AAE5F,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,CAAC;IAChC,OAAO,EAAE,GAAG;SACV,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CACR,0FAA0F;QACzF,sCAAsC,CACvC;IACF,UAAU,EAAE,GAAG;SACb,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CACR,8EAA8E;QAC7E,uDAAuD;QACvD,yCAAyC,CAC1C;IACF,MAAM,EAAE,GAAG;SACT,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;SACtB,QAAQ,EAAE;SACV,OAAO,CAAC,MAAM,CAAC;SACf,QAAQ,CAAC,kGAAkG,CAAC;CAC9G,CAAC,CAAA;AAEF,OAAO,EAAE,eAAe,IAAI,IAAI,EAAE,aAAa,IAAI,OAAO,EAAE,CAAA;AAE5D,SAAS,kBAAkB,CAAC,OAAwC;IACnE,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAA;IACrE,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACd,yFAAyF;YACxF,0FAA0F;YAC1F,+CAA+C,CAChD,CAAA;IACF,CAAC;IACD,OAAO,IAAI,CAAA;AACZ,CAAC;AAED,SAAS,qBAAqB,CAAC,OAAwC;IACtE,OAAO,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAA;AACtE,CAAC;AAED,KAAK,UAAU,UAAU,CACxB,GAAW,EACX,GAAW,EACX,OAAwC;IAExC,IAAI,GAAoD,CAAA;IACxD,IAAI,CAAC;QACJ,GAAG,GAAG,MAAM,MAAM,CAAC,gCAAgC,CAAC,CAAA;IACrD,CAAC;IAAC,MAAM,CAAC;QACR,MAAM,IAAI,KAAK,CACd,qEAAqE;YACpE,iEAAiE,CAClE,CAAA;IACF,CAAC;IAED,MAAM,WAAW,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAA;IAC/C,MAAM,aAAa,GAAG,qBAAqB,CAAC,OAAO,CAAC,CAAA;IAEpD,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,kBAAkB,CAAC,EAAE,WAAW,EAAE,aAAa,EAAE,CAAC,CAAA;IAC3E,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,cAAc,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;QAEtD,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;YAC/B,MAAM,KAAK,GAAa,EAAE,CAAA;YAC1B,KAAK,CAAC,IAAI,CAAC,gBAAgB,MAAM,CAAC,WAAW,EAAE,CAAC,CAAA;YAChD,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACnC,KAAK,CAAC,IAAI,CAAC,qFAAqF,CAAC,CAAA;YAClG,CAAC;iBAAM,CAAC;gBACP,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;oBACtC,MAAM,OAAO,GACZ,KAAK,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,EAAE,CAAA;oBAC5F,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,KAAK,CAAC,IAAI,SAAS,KAAK,CAAC,EAAE,IAAI,OAAO,EAAE,CAAC,CAAA;gBACxF,CAAC;YACF,CAAC;YACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACxB,CAAC;QAED,sFAAsF;QACtF,OAAO,IAAI,CAAC,SAAS,CACpB;YACC,GAAG;YACH,GAAG;YACH,WAAW,EAAE,MAAM,CAAC,WAAW;YAC/B,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBACvC,EAAE,EAAE,CAAC,CAAC,EAAE;gBACR,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,SAAS,EAAE,CAAC,CAAC,SAAS;gBACtB,OAAO,EAAE,CAAC,CAAC,OAAO;gBAClB,GAAG,EAAE,CAAC,CAAC,GAAG;gBACV,GAAG,EAAE,CAAC,CAAC,GAAG;gBACV,GAAG,CAAC,CAAC,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACnE,CAAC,CAAC;SACH,EACD,IAAI,EACJ,CAAC,CACD,CAAA;IACF,CAAC;YAAS,CAAC;QACV,QAAQ,CAAC,KAAK,EAAE,CAAA;IACjB,CAAC;AACF,CAAC;AAED,MAAM,cAAc,GAAmE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE;IAC5G,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,EAAU,CAAA;IAC9C,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,EAAU,CAAA;IAE5C,SAAS,CAAC,GAAG,EAAE;QACd,IAAI,KAAK,EAAE,CAAC;YACX,YAAY,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;QAC3C,CAAC;IACF,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAA;IAEX,SAAS,CAAC,GAAG,EAAE;QACd,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;QACtB,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;QAEtB,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;YACxB,QAAQ,CAAC,mGAAmG,CAAC,CAAA;YAC7G,OAAM;QACP,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,CAAA;QAC1B,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,CAAA;QAE1B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC;YACjD,QAAQ,CAAC,oBAAoB,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,mCAAmC,CAAC,CAAA;YACvF,OAAM;QACP,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,EAAE,CAAC;YAClD,QAAQ,CAAC,qBAAqB,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,qCAAqC,CAAC,CAAA;YAC1F,OAAM;QACP,CAAC;QAED,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC;aAC3B,IAAI,CAAC,SAAS,CAAC;aACf,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE,CAAC,QAAQ,CAAE,GAAa,CAAC,OAAO,CAAC,CAAC,CAAA;IAC5D,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAA;IAEnB,IAAI,KAAK,EAAE,CAAC;QACX,OAAO,KAAC,IAAI,IAAC,KAAK,EAAC,KAAK,YAAE,KAAK,GAAQ,CAAA;IACxC,CAAC;IAED,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,OAAO,KAAC,OAAO,KAAG,CAAA;IACnB,CAAC;IAED,OAAO,KAAC,IAAI,cAAE,MAAM,GAAQ,CAAA;AAC7B,CAAC,CAAA;AAED,eAAe,cAAc,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../commands/serve.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAWH,OAAO,GAAG,MAAM,KAAK,CAAA;AACrB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AA2HrD,eAAO,MAAM,OAAO;;;;qBAAqB,CAAA;AAEzC,QAAA,MAAM,YAAY;;;;sBAAmD,CAAA;AAErE,eAAe,YAAY,CAAA"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../commands/serve.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAWH,OAAO,GAAG,MAAM,KAAK,CAAA;AACrB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AA8HrD,eAAO,MAAM,OAAO;;;;qBAAqB,CAAA;AAEzC,QAAA,MAAM,YAAY;;;;sBAAmD,CAAA;AAErE,eAAe,YAAY,CAAA"}
@@ -14,7 +14,7 @@ import * as process from "node:process";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { useEffect, useState } from "react";
16
16
  import zod from "zod";
17
- import { AddressRouter, ResolveRouter } from "../server/index.js";
17
+ import { AddressRouter, GeocodeRouter, HealthRouter, ResolveRouter } from "../server/index.js";
18
18
  const ClusterManager = ({ options: { cpus = availableParallelism() }, }) => {
19
19
  const [workers, setWorkers] = useState();
20
20
  useEffect(() => {
@@ -71,8 +71,11 @@ const WorkerStatus = ({ worker }) => {
71
71
  const ChildThread = ({ options: { port, host } }) => {
72
72
  useEffect(() => {
73
73
  const app = express();
74
- app.use(express.json());
74
+ // 2mb body cap accommodates a full /api/batch (up to MAILWOMAN_BATCH_MAX addresses).
75
+ app.use(express.json({ limit: "2mb" }));
76
+ app.use(HealthRouter);
75
77
  app.use(AddressRouter);
78
+ app.use(GeocodeRouter);
76
79
  app.use(ResolveRouter);
77
80
  // `mailwoman/server/static/` lives next to the compiled `mailwoman/out/commands/serve.js`,
78
81
  // so resolve relative to this file rather than relying on a repo-root path builder that
@@ -1 +1 @@
1
- {"version":3,"file":"serve.js","sourceRoot":"","sources":["../../commands/serve.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"serve.js","sourceRoot":"","sources":["../../commands/serve.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-release.d.ts","sourceRoot":"","sources":["../data-release.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,iGAAiG;AACjG,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAExD,4FAA4F;AAC5F,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,mBAAmB,GAAG,IAAI,CAYhF;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC/B,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,mBAAmB,GAAG,IAAI,GAClC,MAAM,GAAG,IAAI,CAQf"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Versioned data-artifact addressing + atomic switchover (#485 piece 4). Shard DBs are addressed as
7
+ * `<family>/<family>-us-<slug>-<version>.db`, with a `releases.json` manifest at the data root
8
+ * pinning each family to its current version. So a new build publishes ALONGSIDE the old, flipping
9
+ * the manifest (one atomic file write) cuts traffic over, and the build provenance (the version)
10
+ * travels in the filename — "what data is deployed" is a read of one JSON.
11
+ *
12
+ * Back-compat: with no manifest (or a family unlisted) resolution falls back to the legacy
13
+ * unversioned `<family>-us-<slug>.db`, so the current national build output works unchanged.
14
+ *
15
+ * Example `releases.json`:
16
+ * { "address-points": "2026-05-20.0", "interpolation": "TIGER2023" }
17
+ */
18
+ import { existsSync, readFileSync } from "node:fs";
19
+ /** Read `<dataRoot>/releases.json`. Returns null (legacy mode) when absent or malformed. */
20
+ export function readReleaseManifest(dataRoot) {
21
+ try {
22
+ const raw = JSON.parse(readFileSync(`${dataRoot}/releases.json`, "utf8"));
23
+ if (!raw || typeof raw !== "object")
24
+ return null;
25
+ const out = {};
26
+ for (const [family, version] of Object.entries(raw)) {
27
+ if (typeof version === "string" && version)
28
+ out[family] = version;
29
+ }
30
+ return Object.keys(out).length > 0 ? out : null;
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ /**
37
+ * Resolve a shard's on-disk path: the manifest-pinned `<family>-us-<slug>-<version>.db` when present,
38
+ * else the legacy unversioned `<family>-us-<slug>.db`, else null if neither exists.
39
+ */
40
+ export function resolveShardPath(dataRoot, family, slug, manifest) {
41
+ const version = manifest?.[family];
42
+ if (version) {
43
+ const versioned = `${dataRoot}/${family}/${family}-us-${slug}-${version}.db`;
44
+ if (existsSync(versioned))
45
+ return versioned;
46
+ }
47
+ const legacy = `${dataRoot}/${family}/${family}-us-${slug}.db`;
48
+ return existsSync(legacy) ? legacy : null;
49
+ }
50
+ //# sourceMappingURL=data-release.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-release.js","sourceRoot":"","sources":["../data-release.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAKlD,4FAA4F;AAC5F,MAAM,UAAU,mBAAmB,CAAC,QAAgB;IACnD,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,GAAG,QAAQ,gBAAgB,EAAE,MAAM,CAAC,CAAY,CAAA;QACpF,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAA;QAChD,MAAM,GAAG,GAAwB,EAAE,CAAA;QACnC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAA8B,CAAC,EAAE,CAAC;YAChF,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO;gBAAE,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,CAAA;QAClE,CAAC;QACD,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAA;IAChD,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,IAAI,CAAA;IACZ,CAAC;AACF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAC/B,QAAgB,EAChB,MAAc,EACd,IAAY,EACZ,QAAoC;IAEpC,MAAM,OAAO,GAAG,QAAQ,EAAE,CAAC,MAAM,CAAC,CAAA;IAClC,IAAI,OAAO,EAAE,CAAC;QACb,MAAM,SAAS,GAAG,GAAG,QAAQ,IAAI,MAAM,IAAI,MAAM,OAAO,IAAI,IAAI,OAAO,KAAK,CAAA;QAC5E,IAAI,UAAU,CAAC,SAAS,CAAC;YAAE,OAAO,SAAS,CAAA;IAC5C,CAAC;IACD,MAAM,MAAM,GAAG,GAAG,QAAQ,IAAI,MAAM,IAAI,MAAM,OAAO,IAAI,KAAK,CAAA;IAC9D,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAA;AAC1C,CAAC"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geocode-core.d.ts","sourceRoot":"","sources":["../geocode-core.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,225 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Shared street-level geocode cascade — the reusable core behind the `geocode` CLI command AND the
7
+ * server's `/api/geocode` + `/api/batch` endpoints (#485). One implementation of the cascade, so the
8
+ * CLI and the service never drift.
9
+ *
10
+ * Cascade (the eval-validated path — 98.8% within 100m on the non-circular Travis holdout):
11
+ * 1. RAW neural parse (`classifier.parse`, postcodeRepair). NOT the runtime pipeline — its
12
+ * reconcile stage merges street INTO house_number, dropping the street node the coordinate
13
+ * tiers need (#566).
14
+ * 2. Read the parsed region → pick the per-state situs + interpolation shards.
15
+ * 3. `resolveTree` with the coordinate tiers wired (additive; admin-only when shards absent).
16
+ * 4. Extract the best coordinate + resolution tier (address_point > interpolated > admin).
17
+ *
18
+ * The cascade depends on a {@link ShardResolver} — a `(stateSlug) => { addressPoints?, interpolation? }`
19
+ * function — so the CLI (honoring its explicit `--address-points-db` flags) and the server (a cached
20
+ * {@link ShardProvider}) supply shards their own way without the core knowing how.
21
+ */
22
+ import { existsSync } from "node:fs";
23
+ import { readReleaseManifest, resolveShardPath } from "./data-release.js";
24
+ /** Lowercase 2-letter state slug from a parsed region value / resolver name, else null. */
25
+ export function regionToStateSlug(regionValue, resolverName) {
26
+ for (const candidate of [regionValue, resolverName]) {
27
+ if (!candidate)
28
+ continue;
29
+ const trimmed = candidate.trim();
30
+ if (/^[A-Za-z]{2}$/.test(trimmed))
31
+ return trimmed.toLowerCase();
32
+ }
33
+ return null;
34
+ }
35
+ /** Walk a (parsed or resolved) tree for its region → the per-state shard slug (e.g. `"tx"`), else null. */
36
+ export function regionSlugFromTree(tree) {
37
+ let regionValue = null;
38
+ let regionResolverName = null;
39
+ const stack = [...tree.roots];
40
+ while (stack.length > 0) {
41
+ const node = stack.pop();
42
+ if (node.tag === "region" && !regionValue) {
43
+ regionValue = node.value.trim() || null;
44
+ regionResolverName = node.metadata?.["resolver_name"] ?? null;
45
+ }
46
+ stack.push(...node.children);
47
+ }
48
+ return regionToStateSlug(regionValue, regionResolverName);
49
+ }
50
+ /** Per-state situs shard path under `<dataRoot>/address-points/`, or null if the slug/file is absent. */
51
+ export function selectAddressPointsDb(dataRoot, stateSlug) {
52
+ if (!stateSlug)
53
+ return null;
54
+ const candidate = `${dataRoot}/address-points/address-points-us-${stateSlug}.db`;
55
+ return existsSync(candidate) ? candidate : null;
56
+ }
57
+ /** Per-state interpolation shard path under `<dataRoot>/interpolation/`, or null if absent. */
58
+ export function selectInterpolationDb(dataRoot, stateSlug) {
59
+ if (!stateSlug)
60
+ return null;
61
+ const candidate = `${dataRoot}/interpolation/interpolation-us-${stateSlug}.db`;
62
+ return existsSync(candidate) ? candidate : null;
63
+ }
64
+ /**
65
+ * Opens + CACHES per-state situs/interpolation lookups so a batch geocoding many addresses in one
66
+ * state opens that state's (possibly multi-GB) shards once, not once per row. Versioned-data aware
67
+ * (#485): paths resolve through the `releases.json` manifest (legacy unversioned fallback), and
68
+ * {@link reload} performs a zero-downtime atomic switchover when a new version is published. Call
69
+ * {@link close} when done to release every cached handle.
70
+ */
71
+ export class ShardProvider {
72
+ #factory;
73
+ #dataRoot;
74
+ #cache = new Map();
75
+ /** Previous-generation handles, retired by reload() and closed on the NEXT reload (one-gen grace). */
76
+ #retired = [];
77
+ #manifest;
78
+ constructor(factory, dataRoot) {
79
+ this.#factory = factory;
80
+ this.#dataRoot = dataRoot;
81
+ this.#manifest = readReleaseManifest(dataRoot);
82
+ }
83
+ #open(stateSlug) {
84
+ const apPath = resolveShardPath(this.#dataRoot, "address-points", stateSlug, this.#manifest);
85
+ const ipPath = resolveShardPath(this.#dataRoot, "interpolation", stateSlug, this.#manifest);
86
+ const ap = apPath ? new this.#factory.AddressPointSqliteLookup(apPath) : undefined;
87
+ const ip = ipPath ? new this.#factory.StreetInterpolator({ dbPath: ipPath }) : undefined;
88
+ return { addressPoints: ap, interpolation: ip, _ap: ap, _ip: ip, apPath, ipPath };
89
+ }
90
+ for = (stateSlug) => {
91
+ if (!stateSlug)
92
+ return {};
93
+ let entry = this.#cache.get(stateSlug);
94
+ if (!entry) {
95
+ entry = this.#open(stateSlug);
96
+ this.#cache.set(stateSlug, entry);
97
+ }
98
+ return { addressPoints: entry.addressPoints, interpolation: entry.interpolation };
99
+ };
100
+ /** The current data-release versions ({@link readReleaseManifest}), or null in legacy mode. */
101
+ versions() {
102
+ return this.#manifest ? { ...this.#manifest } : null;
103
+ }
104
+ /**
105
+ * Re-read the manifest and atomically swap any cached shard whose resolved path changed. New
106
+ * requests see the new version immediately; the old handles are RETIRED and closed on the next
107
+ * reload (one-generation grace — safe because find() is synchronous, so no in-flight query can
108
+ * still hold a handle once a request yields). Returns the new version map.
109
+ */
110
+ reload() {
111
+ for (const h of this.#retired)
112
+ h.close();
113
+ this.#retired = [];
114
+ this.#manifest = readReleaseManifest(this.#dataRoot);
115
+ for (const [slug, old] of this.#cache) {
116
+ const apPath = resolveShardPath(this.#dataRoot, "address-points", slug, this.#manifest);
117
+ const ipPath = resolveShardPath(this.#dataRoot, "interpolation", slug, this.#manifest);
118
+ if (apPath === old.apPath && ipPath === old.ipPath)
119
+ continue; // unchanged — keep the open handle
120
+ this.#cache.set(slug, this.#open(slug));
121
+ if (old._ap)
122
+ this.#retired.push(old._ap);
123
+ if (old._ip)
124
+ this.#retired.push(old._ip);
125
+ }
126
+ return this.versions();
127
+ }
128
+ close() {
129
+ for (const e of this.#cache.values()) {
130
+ e._ap?.close();
131
+ e._ip?.close();
132
+ }
133
+ for (const h of this.#retired)
134
+ h.close();
135
+ this.#cache.clear();
136
+ this.#retired = [];
137
+ }
138
+ }
139
+ /**
140
+ * Run the full street-level cascade on one address and return the structured geocode result. Always
141
+ * returns a result (admin tier even with no coordinate shards). Throws only on a fatal parse/resolve
142
+ * error — callers doing batch work should catch per-row.
143
+ */
144
+ export async function geocodeAddress(input, deps) {
145
+ const tree = await deps.classifier.parse(input, { postcodeRepair: true });
146
+ const stateSlug = regionSlugFromTree(tree);
147
+ const { addressPoints, interpolation } = deps.shards?.(stateSlug) ?? {};
148
+ const opts = {};
149
+ if (deps.defaultCountry)
150
+ opts.defaultCountry = deps.defaultCountry;
151
+ if (addressPoints)
152
+ opts.addressPoints = addressPoints;
153
+ if (interpolation) {
154
+ opts.interpolation = interpolation;
155
+ if (deps.interpCalibration && deps.interpCalibration !== 1) {
156
+ opts.interpolationRadiusCalibration = deps.interpCalibration;
157
+ }
158
+ }
159
+ const resolved = await deps.resolver.resolveTree(tree, opts);
160
+ return extractGeocodeResult(input, resolved);
161
+ }
162
+ /**
163
+ * Walk the resolved tree and extract the geocode result: the street node's address-point /
164
+ * interpolation coordinate (whichever tier won), else the best admin centroid (locality → region →
165
+ * country).
166
+ */
167
+ export function extractGeocodeResult(input, tree) {
168
+ const allNodes = [];
169
+ const flatten = (nodes) => {
170
+ for (const n of nodes) {
171
+ allNodes.push(n);
172
+ flatten(n.children);
173
+ }
174
+ };
175
+ flatten(tree.roots);
176
+ const streetNode = allNodes.find((n) => n.tag === "street");
177
+ let lat = null;
178
+ let lon = null;
179
+ let tier = "admin";
180
+ let uncertaintyM = null;
181
+ if (streetNode?.metadata?.["resolution_tier"] === "address_point") {
182
+ const ap = streetNode.metadata["address_point"];
183
+ if (ap) {
184
+ lat = ap.lat;
185
+ lon = ap.lon;
186
+ tier = "address_point";
187
+ uncertaintyM = 1; // Floor: situs point is essentially exact.
188
+ }
189
+ }
190
+ if (tier !== "address_point" && streetNode?.metadata?.["resolution_tier"] === "interpolated") {
191
+ const ip = streetNode.metadata["interpolated_point"];
192
+ if (ip) {
193
+ lat = ip.lat;
194
+ lon = ip.lon;
195
+ tier = "interpolated";
196
+ uncertaintyM = streetNode.metadata["uncertainty_m"] ?? null;
197
+ }
198
+ }
199
+ if (tier === "admin") {
200
+ const adminPriority = ["locality", "dependent_locality", "region", "country"];
201
+ for (const tag of adminPriority) {
202
+ const node = allNodes.find((n) => n.tag === tag && n.lat != null && n.lon != null);
203
+ if (node) {
204
+ lat = node.lat;
205
+ lon = node.lon;
206
+ break;
207
+ }
208
+ }
209
+ }
210
+ const locality = allNodes.find((n) => n.tag === "locality" || n.tag === "dependent_locality")?.value?.trim() || null;
211
+ const region = allNodes.find((n) => n.tag === "region")?.value?.trim() || null;
212
+ const postcode = allNodes.find((n) => n.tag === "postcode")?.value?.trim() || null;
213
+ const HIERARCHY_TAGS = ["locality", "dependent_locality", "subregion", "region", "country"];
214
+ const hierarchy = allNodes
215
+ .filter((n) => HIERARCHY_TAGS.includes(n.tag) && (n.lat != null || n.placeId))
216
+ .sort((a, b) => HIERARCHY_TAGS.indexOf(a.tag) - HIERARCHY_TAGS.indexOf(b.tag))
217
+ .map((n) => ({
218
+ tag: n.tag,
219
+ value: n.value.trim(),
220
+ ...(n.lat != null ? { lat: n.lat, lon: n.lon } : {}),
221
+ ...(n.placeId ? { placeId: n.placeId } : {}),
222
+ }));
223
+ return { input, lat, lon, resolution_tier: tier, uncertainty_m: uncertaintyM, locality, region, postcode, hierarchy };
224
+ }
225
+ //# sourceMappingURL=geocode-core.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geocode-core.js","sourceRoot":"","sources":["../geocode-core.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GeocodeRouter.d.ts","sourceRoot":"","sources":["../../server/GeocodeRouter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,EAAuB,MAAM,EAAE,MAAM,SAAS,CAAA;AAyNrD,eAAO,MAAM,aAAa,EAAE,MAAiB,CAAA"}
@@ -0,0 +1,210 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Street-level geocoding endpoints (#485, piece 1):
7
+ * - `POST /api/geocode` — `{ address }` → one {@link GeocodeResult} (parse → situs → interp → admin).
8
+ * - `POST /api/batch` — `{ addresses: string[] }` → results in input order, BOUNDED concurrency,
9
+ * PER-ROW error isolation (one bad address never fails the batch — it gets an `{ error }` slot).
10
+ *
11
+ * Runs the SAME cascade as the `geocode` CLI (`mailwoman/geocode-core.ts`), so the service and the
12
+ * CLI never drift. Deps (classifier + resolver + a cached {@link ShardProvider}) are lazy-loaded
13
+ * ONCE on first hit and reused; the per-state shard cache means a batch hitting many CA addresses
14
+ * opens CA's situs shard once, not per row.
15
+ */
16
+ import { createWofResolver } from "@mailwoman/core/resolver";
17
+ import { Router } from "express";
18
+ import { existsSync } from "node:fs";
19
+ import { geocodeAddress, regionSlugFromTree, ShardProvider, } from "../geocode-core.js";
20
+ import { recordGeocode } from "./metrics.js";
21
+ /** Default per-state shard root + interp calibration — mirror the CLI defaults. */
22
+ const DATA_ROOT = process.env["MAILWOMAN_DATA_ROOT"] ?? "/mnt/playpen/mailwoman-data";
23
+ const INTERP_CALIBRATION = 1.7;
24
+ /** Bounded concurrency for `/api/batch`. Override with MAILWOMAN_BATCH_CONCURRENCY. */
25
+ const BATCH_CONCURRENCY = Math.max(1, Number(process.env["MAILWOMAN_BATCH_CONCURRENCY"] ?? "8"));
26
+ /** Hard cap on batch size — a guardrail against unbounded request bodies. */
27
+ const BATCH_MAX = Math.max(1, Number(process.env["MAILWOMAN_BATCH_MAX"] ?? "1000"));
28
+ function wofPaths() {
29
+ const env = process.env["MAILWOMAN_WOF_DB"];
30
+ if (env)
31
+ return env.split(",").map((p) => p.trim()).filter(Boolean);
32
+ return ["/mnt/playpen/mailwoman-data/wof/admin-global-priority.db", "/mnt/playpen/mailwoman-data/wof/postalcode-us.db"].filter((p) => existsSync(p));
33
+ }
34
+ let depsPromise = null;
35
+ async function getDeps() {
36
+ if (depsPromise)
37
+ return depsPromise;
38
+ depsPromise = (async () => {
39
+ let neuralMod;
40
+ let resolverMod;
41
+ try {
42
+ neuralMod = await import("@mailwoman/neural");
43
+ resolverMod = await import("@mailwoman/resolver-wof-sqlite");
44
+ }
45
+ catch {
46
+ console.error("GeocodeRouter: @mailwoman/neural + @mailwoman/resolver-wof-sqlite are required");
47
+ return null;
48
+ }
49
+ const paths = wofPaths();
50
+ if (paths.length === 0) {
51
+ console.error("GeocodeRouter: no WOF DBs found — set MAILWOMAN_WOF_DB");
52
+ return null;
53
+ }
54
+ const classifier = await neuralMod.NeuralAddressClassifier.loadFromWeights({ locale: "en-US" });
55
+ const backend = new resolverMod.WofSqlitePlaceLookup({ databasePath: paths.length === 1 ? paths[0] : paths });
56
+ const resolver = createWofResolver(backend);
57
+ const shards = new ShardProvider(resolverMod, DATA_ROOT);
58
+ return { classifier, resolver, shards, defaultCountry: "US" };
59
+ })();
60
+ return depsPromise;
61
+ }
62
+ function oneGeocode(deps, address) {
63
+ return geocodeAddress(address, {
64
+ classifier: deps.classifier,
65
+ resolver: deps.resolver,
66
+ shards: deps.shards.for,
67
+ defaultCountry: deps.defaultCountry,
68
+ interpCalibration: INTERP_CALIBRATION,
69
+ });
70
+ }
71
+ const DEPS_UNAVAILABLE = {
72
+ error: "Geocoder not available. Install @mailwoman/neural + @mailwoman/resolver-wof-sqlite and set MAILWOMAN_WOF_DB.",
73
+ };
74
+ const singleHandler = async (req, res) => {
75
+ const address = typeof req.body?.address === "string" ? req.body.address.trim() : "";
76
+ if (!address) {
77
+ res.status(400).json({ error: "Missing `address` (string)" });
78
+ return;
79
+ }
80
+ const deps = await getDeps();
81
+ if (!deps) {
82
+ res.status(503).json(DEPS_UNAVAILABLE);
83
+ return;
84
+ }
85
+ const t0 = performance.now();
86
+ try {
87
+ const result = await oneGeocode(deps, address);
88
+ recordGeocode(performance.now() - t0, result.resolution_tier);
89
+ res.status(200).json(result);
90
+ }
91
+ catch (err) {
92
+ recordGeocode(performance.now() - t0, "error");
93
+ res.status(500).json({ error: `geocode error: ${err instanceof Error ? err.message : String(err)}` });
94
+ }
95
+ };
96
+ const batchHandler = async (req, res) => {
97
+ const addresses = req.body?.addresses;
98
+ if (!Array.isArray(addresses) || addresses.some((a) => typeof a !== "string")) {
99
+ res.status(400).json({ error: "Body must be `{ addresses: string[] }`" });
100
+ return;
101
+ }
102
+ if (addresses.length === 0) {
103
+ res.status(200).json({ results: [] });
104
+ return;
105
+ }
106
+ if (addresses.length > BATCH_MAX) {
107
+ res.status(413).json({ error: `Batch too large: ${addresses.length} > ${BATCH_MAX} (MAILWOMAN_BATCH_MAX)` });
108
+ return;
109
+ }
110
+ const deps = await getDeps();
111
+ if (!deps) {
112
+ res.status(503).json(DEPS_UNAVAILABLE);
113
+ return;
114
+ }
115
+ const inputs = addresses.map((a) => a.trim());
116
+ const results = new Array(inputs.length);
117
+ // Bounded-concurrency worker pool over a shared cursor — results land in input order; a thrown
118
+ // row is isolated to its own slot. The shard cache makes same-state rows cheap after the first.
119
+ let cursor = 0;
120
+ const worker = async () => {
121
+ for (let i = cursor++; i < inputs.length; i = cursor++) {
122
+ const input = inputs[i];
123
+ const t0 = performance.now();
124
+ try {
125
+ const result = await oneGeocode(deps, input);
126
+ recordGeocode(performance.now() - t0, result.resolution_tier);
127
+ results[i] = result;
128
+ }
129
+ catch (err) {
130
+ recordGeocode(performance.now() - t0, "error");
131
+ results[i] = { input, error: err instanceof Error ? err.message : String(err) };
132
+ }
133
+ }
134
+ };
135
+ await Promise.all(Array.from({ length: Math.min(BATCH_CONCURRENCY, inputs.length) }, worker));
136
+ res.status(200).json({ results });
137
+ };
138
+ /**
139
+ * `POST /api/resolve-tree` — the resolver-service endpoint that `RemoteResolver` (core) calls. Accepts
140
+ * an already-parsed `{ tree, opts? }`, selects this region's situs/interpolation shards (the data lives
141
+ * here, not on the caller), runs the FULL cascade, and returns `{ tree }`. This is what lets a
142
+ * stateless parser node geocode at street level against a shared resolver service.
143
+ */
144
+ const resolveTreeHandler = async (req, res) => {
145
+ const tree = req.body?.tree;
146
+ if (!tree || !Array.isArray(tree.roots)) {
147
+ res.status(400).json({ error: "Body must be `{ tree: AddressTree, opts?: ResolveOpts }`" });
148
+ return;
149
+ }
150
+ const incomingOpts = (req.body?.opts ?? {});
151
+ const deps = await getDeps();
152
+ if (!deps) {
153
+ res.status(503).json(DEPS_UNAVAILABLE);
154
+ return;
155
+ }
156
+ const t0 = performance.now();
157
+ try {
158
+ const { addressPoints, interpolation } = deps.shards.for(regionSlugFromTree(tree));
159
+ const opts = {
160
+ ...incomingOpts,
161
+ defaultCountry: incomingOpts.defaultCountry ?? deps.defaultCountry,
162
+ ...(addressPoints ? { addressPoints } : {}),
163
+ ...(interpolation
164
+ ? { interpolation, interpolationRadiusCalibration: incomingOpts.interpolationRadiusCalibration ?? INTERP_CALIBRATION }
165
+ : {}),
166
+ };
167
+ const resolved = await deps.resolver.resolveTree(tree, opts);
168
+ // Best-effort tier metric: read the street node's stamped tier (matches the geocode path).
169
+ const street = resolved.roots.flatMap((r) => collectStreetTier(r)).find(Boolean);
170
+ recordGeocode(performance.now() - t0, street ?? "admin");
171
+ res.status(200).json({ tree: resolved });
172
+ }
173
+ catch (err) {
174
+ recordGeocode(performance.now() - t0, "error");
175
+ res.status(500).json({ error: `resolve-tree error: ${err instanceof Error ? err.message : String(err)}` });
176
+ }
177
+ };
178
+ /** Pull the street node's resolution tier (if any) for the metric — mirrors extractGeocodeResult. */
179
+ function collectStreetTier(node) {
180
+ const out = [];
181
+ if (node.tag === "street") {
182
+ const tier = node.metadata?.["resolution_tier"];
183
+ if (tier === "address_point" || tier === "interpolated")
184
+ out.push(tier);
185
+ }
186
+ for (const child of node.children)
187
+ out.push(...collectStreetTier(child));
188
+ return out;
189
+ }
190
+ /**
191
+ * `POST /api/reload` — versioned data switchover (#485). Re-reads `releases.json` and atomically swaps
192
+ * any per-state shard whose pinned version changed (zero-downtime, one-generation grace on the old
193
+ * handles). Call this after publishing a new shard build to cut traffic over without a restart. Returns
194
+ * the new version map. (Deploy-only; gate it behind your ingress — no auth here by design, per #485.)
195
+ */
196
+ const reloadHandler = async (_req, res) => {
197
+ const deps = await getDeps();
198
+ if (!deps) {
199
+ res.status(503).json(DEPS_UNAVAILABLE);
200
+ return;
201
+ }
202
+ const versions = deps.shards.reload();
203
+ res.status(200).json({ reloaded: true, versions });
204
+ };
205
+ export const GeocodeRouter = Router();
206
+ GeocodeRouter.post("/api/geocode", singleHandler);
207
+ GeocodeRouter.post("/api/batch", batchHandler);
208
+ GeocodeRouter.post("/api/resolve-tree", resolveTreeHandler);
209
+ GeocodeRouter.post("/api/reload", reloadHandler);
210
+ //# sourceMappingURL=GeocodeRouter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GeocodeRouter.js","sourceRoot":"","sources":["../../server/GeocodeRouter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HealthRouter.d.ts","sourceRoot":"","sources":["../../server/HealthRouter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAuB,MAAM,EAAE,MAAM,SAAS,CAAA;AA6ErD,eAAO,MAAM,YAAY,EAAE,MAAiB,CAAA"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Operational endpoints (#485 observability):
7
+ * - `GET /health` — "what's deployed, in one curl": model-card name/version/locale + which WOF
8
+ * gazetteer and how many per-state situs/interpolation shards are on disk. LIGHTWEIGHT — reads
9
+ * JSON + counts files, never loads the model, never throws (health must answer even when broken).
10
+ * - `GET /metrics` — the live geocode metrics snapshot (latency percentiles + per-tier counts).
11
+ */
12
+ import { Router } from "express";
13
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
14
+ import { createRequire } from "node:module";
15
+ import { readReleaseManifest } from "../data-release.js";
16
+ import { metricsSnapshot } from "./metrics.js";
17
+ const DATA_ROOT = process.env["MAILWOMAN_DATA_ROOT"] ?? "/mnt/playpen/mailwoman-data";
18
+ const startedAt = Date.now();
19
+ /** Best-effort model-card read: env override → installed weights package → dev-tree fallback. */
20
+ function readModelCard() {
21
+ const candidates = [];
22
+ if (process.env["MAILWOMAN_MODEL_CARD"])
23
+ candidates.push(process.env["MAILWOMAN_MODEL_CARD"]);
24
+ try {
25
+ candidates.push(createRequire(import.meta.url).resolve("@mailwoman/neural-weights-en-us/model-card.json"));
26
+ }
27
+ catch {
28
+ /* package not resolvable from here — fall through */
29
+ }
30
+ candidates.push("neural-weights-en-us/model-card.json");
31
+ for (const p of candidates) {
32
+ try {
33
+ if (existsSync(p))
34
+ return JSON.parse(readFileSync(p, "utf8"));
35
+ }
36
+ catch {
37
+ /* unreadable / malformed — try the next candidate */
38
+ }
39
+ }
40
+ return null;
41
+ }
42
+ /** Count canonical per-state shards (`<prefix>-us-<2-letter>.db`) in a data subdir; 0 if absent. */
43
+ function countShards(subdir, prefix) {
44
+ try {
45
+ const re = new RegExp(`^${prefix}-us-[a-z]{2}\\.db$`);
46
+ return readdirSync(`${DATA_ROOT}/${subdir}`).filter((f) => re.test(f)).length;
47
+ }
48
+ catch {
49
+ return 0;
50
+ }
51
+ }
52
+ function wofPaths() {
53
+ const env = process.env["MAILWOMAN_WOF_DB"];
54
+ const paths = env
55
+ ? env.split(",").map((p) => p.trim()).filter(Boolean)
56
+ : ["/mnt/playpen/mailwoman-data/wof/admin-global-priority.db", "/mnt/playpen/mailwoman-data/wof/postalcode-us.db"];
57
+ return paths.filter((p) => existsSync(p));
58
+ }
59
+ const healthHandler = (_req, res) => {
60
+ const card = readModelCard();
61
+ res.status(200).json({
62
+ status: "ok",
63
+ uptime_s: Math.round((Date.now() - startedAt) / 1000),
64
+ model: card
65
+ ? {
66
+ name: card["name"],
67
+ version: card["version"],
68
+ locale: card["locale"],
69
+ labels: Array.isArray(card["labels"]) ? card["labels"].length : undefined,
70
+ format: card["format"],
71
+ }
72
+ : null,
73
+ data: {
74
+ data_root: DATA_ROOT,
75
+ // Versioned-switchover provenance (#485): the releases.json pin, or null in legacy mode.
76
+ versions: readReleaseManifest(DATA_ROOT),
77
+ wof_dbs: wofPaths(),
78
+ situs_states: countShards("address-points", "address-points"),
79
+ interpolation_states: countShards("interpolation", "interpolation"),
80
+ },
81
+ });
82
+ };
83
+ const metricsHandler = (_req, res) => {
84
+ res.status(200).json(metricsSnapshot());
85
+ };
86
+ export const HealthRouter = Router();
87
+ HealthRouter.get("/health", healthHandler);
88
+ HealthRouter.get("/metrics", metricsHandler);
89
+ //# sourceMappingURL=HealthRouter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HealthRouter.js","sourceRoot":"","sources":["../../server/HealthRouter.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../server/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../server/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA"}
@@ -4,5 +4,7 @@
4
4
  * @author Teffen Ellis, et al.
5
5
  */
6
6
  export * from "./AddressRouter.js";
7
+ export * from "./GeocodeRouter.js";
8
+ export * from "./HealthRouter.js";
7
9
  export * from "./ResolveRouter.js";
8
10
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,oBAAoB,CAAA;AAClC,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../../server/metrics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAaxD,sGAAsG;AACtG,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,GAAG,IAAI,CAarF;AAQD,MAAM,WAAW,eAAe;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE;QACR,KAAK,EAAE,MAAM,CAAA;QACb,MAAM,EAAE,MAAM,CAAA;QACd,KAAK,EAAE,MAAM,CAAC,cAAc,EAAE,MAAM,CAAC,CAAA;QACrC,UAAU,EAAE;YAAE,GAAG,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAA;QACzE,eAAe,EAAE,MAAM,CAAA;KACvB,CAAA;CACD;AAED,0EAA0E;AAC1E,wBAAgB,eAAe,IAAI,eAAe,CAmBjD;AAED,uDAAuD;AACvD,wBAAgB,qBAAqB,IAAI,IAAI,CAQ5C"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * In-process geocode metrics (#485 observability). Dependency-free: monotonic counters per
7
+ * resolution tier + a bounded reservoir of recent latencies for percentile estimation. The issue
8
+ * asks to MEASURE the SLO targets (p99 ≤ 100ms admin, ≤ 250ms with situs) rather than invent them —
9
+ * this is the instrument. Surfaced by `GET /metrics`; reset on process restart (no persistence —
10
+ * scrape it).
11
+ */
12
+ /** Recent-latency reservoir size. ~2k samples gives stable p99 without unbounded memory. */
13
+ const MAX_SAMPLES = 2048;
14
+ const latencies = [];
15
+ let writeIdx = 0;
16
+ const tierCounts = { address_point: 0, interpolated: 0, admin: 0 };
17
+ let total = 0;
18
+ let errors = 0;
19
+ const startedAt = Date.now();
20
+ /** Record one completed geocode: its wall-clock latency and which tier produced it (or `"error"`). */
21
+ export function recordGeocode(latencyMs, tier) {
22
+ total++;
23
+ if (tier === "error") {
24
+ errors++;
25
+ }
26
+ else {
27
+ tierCounts[tier]++;
28
+ }
29
+ if (latencies.length < MAX_SAMPLES) {
30
+ latencies.push(latencyMs);
31
+ }
32
+ else {
33
+ latencies[writeIdx] = latencyMs;
34
+ writeIdx = (writeIdx + 1) % MAX_SAMPLES;
35
+ }
36
+ }
37
+ function percentile(sorted, p) {
38
+ if (sorted.length === 0)
39
+ return 0;
40
+ const idx = Math.min(sorted.length - 1, Math.floor(p * sorted.length));
41
+ return Math.round(sorted[idx] * 100) / 100;
42
+ }
43
+ /** Current metrics snapshot — sorted-reservoir percentiles + counters. */
44
+ export function metricsSnapshot() {
45
+ const sorted = [...latencies].sort((a, b) => a - b);
46
+ return {
47
+ uptime_s: Math.round((Date.now() - startedAt) / 1000),
48
+ geocode: {
49
+ total,
50
+ errors,
51
+ tiers: { ...tierCounts },
52
+ latency_ms: sorted.length
53
+ ? {
54
+ p50: percentile(sorted, 0.5),
55
+ p90: percentile(sorted, 0.9),
56
+ p99: percentile(sorted, 0.99),
57
+ max: Math.round(sorted[sorted.length - 1] * 100) / 100,
58
+ }
59
+ : null,
60
+ latency_samples: sorted.length,
61
+ },
62
+ };
63
+ }
64
+ /** Test-only reset of all counters + the reservoir. */
65
+ export function __resetMetricsForTest() {
66
+ latencies.length = 0;
67
+ writeIdx = 0;
68
+ tierCounts.address_point = 0;
69
+ tierCounts.interpolated = 0;
70
+ tierCounts.admin = 0;
71
+ total = 0;
72
+ errors = 0;
73
+ }
74
+ //# sourceMappingURL=metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metrics.js","sourceRoot":"","sources":["../../server/metrics.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mailwoman",
3
- "version": "4.6.0",
3
+ "version": "4.7.0",
4
4
  "description": "Utilities for working with contacts for email, phone, and postal addresses.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "contributors": [
@@ -33,13 +33,13 @@
33
33
  "dependencies": {
34
34
  "@fragaria/address-formatter": "^6.7.1",
35
35
  "@inkjs/ui": "^2.0.0",
36
- "@mailwoman/classifiers": "4.6.0",
37
- "@mailwoman/core": "4.6.0",
38
- "@mailwoman/kind-classifier": "4.6.0",
39
- "@mailwoman/locale-gate": "4.6.0",
40
- "@mailwoman/normalize": "4.6.0",
41
- "@mailwoman/phrase-grouper": "4.6.0",
42
- "@mailwoman/query-shape": "4.6.0",
36
+ "@mailwoman/classifiers": "4.7.0",
37
+ "@mailwoman/core": "4.7.0",
38
+ "@mailwoman/kind-classifier": "4.7.0",
39
+ "@mailwoman/locale-gate": "4.7.0",
40
+ "@mailwoman/normalize": "4.7.0",
41
+ "@mailwoman/phrase-grouper": "4.7.0",
42
+ "@mailwoman/query-shape": "4.7.0",
43
43
  "change-case": "^5.4.4",
44
44
  "express": "^5.2.1",
45
45
  "geojson": "^0.5.0",