@mailwoman/api 9.1.0 → 9.3.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/README.md +1 -1
- package/{app.ts → lib/app.ts} +39 -11
- package/{engine.ts → lib/engine.ts} +24 -12
- package/{index.ts → lib/index.ts} +4 -4
- package/{routes.ts → lib/routes.ts} +78 -54
- package/lib/schema.ts +502 -0
- package/out/app.d.ts +10 -2
- package/out/app.d.ts.map +1 -1
- package/out/app.js +21 -9
- package/out/app.js.map +1 -1
- package/out/engine.d.ts +19 -23
- package/out/engine.d.ts.map +1 -1
- package/out/engine.js.map +1 -1
- package/out/index.d.ts +4 -4
- package/out/index.d.ts.map +1 -1
- package/out/index.js +4 -4
- package/out/index.js.map +1 -1
- package/out/routes.d.ts +9 -3
- package/out/routes.d.ts.map +1 -1
- package/out/routes.js +47 -50
- package/out/routes.js.map +1 -1
- package/out/schema.d.ts +616 -9
- package/out/schema.d.ts.map +1 -1
- package/out/schema.js +177 -21
- package/out/schema.js.map +1 -1
- package/package.json +38 -9
- package/schema.ts +0 -328
package/schema.ts
DELETED
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @copyright Sister Software
|
|
3
|
-
* @license AGPL-3.0
|
|
4
|
-
* @author Teffen Ellis, et al.
|
|
5
|
-
*
|
|
6
|
-
* Zod wire schemas for the native `/v1` surface. Unlike the drop-ins (photon, nominatim,
|
|
7
|
-
* libpostal), nothing here is a vendor contract — this surface is ours to design, so request
|
|
8
|
-
* bodies are REQUIRED and validator-enforced (no legacy tolerance to preserve). A `defaultHook`
|
|
9
|
-
* on the app maps validation failures through the shared `APIErrorSchema` envelope
|
|
10
|
-
* (`apiError(c, 400, "invalid request body", <zod summary>)`) — the pattern boundary every
|
|
11
|
-
* surface holds to: where no legacy contract exists, the validator MAY speak, but only in
|
|
12
|
-
* our envelope.
|
|
13
|
-
*
|
|
14
|
-
* `APIErrorSchema` itself is owned by `@mailwoman/api-kit` (plumbing shared by every native
|
|
15
|
-
* surface) — it's re-exported here so route modules can import every schema they need, request
|
|
16
|
-
* and error alike, from this one file.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { z } from "@hono/zod-openapi"
|
|
20
|
-
|
|
21
|
-
export { APIErrorSchema } from "@mailwoman/api-kit"
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* `POST /v1/parse` request body.
|
|
25
|
-
*/
|
|
26
|
-
/**
|
|
27
|
-
* The input register (Decision A / GTM B10): `fragmented` = the map-search register (evidence-bundle channels feed);
|
|
28
|
-
* `formatted` = the validation/record register (channels off). Unset → the engine derives it from the input's shape.
|
|
29
|
-
* `/v1/batch` defaults to `formatted` (batch rows are the record register by nature).
|
|
30
|
-
*/
|
|
31
|
-
export const InputModeSchema = z.enum(["fragmented", "formatted"]).openapi("InputMode")
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Longest accepted `address`, in characters.
|
|
35
|
-
*
|
|
36
|
-
* Sized against what the model can actually read, not against a guess at abuse. The classifier's window is 128
|
|
37
|
-
* SentencePiece pieces — roughly 330 characters of address text — and everything past it is truncated before inference,
|
|
38
|
-
* so input beyond this bound cannot influence a result. The margin over that window leaves room for scripts that
|
|
39
|
-
* tokenize denser than Latin, and for the department-and-division prefixes web forms concatenate.
|
|
40
|
-
*
|
|
41
|
-
* The bound exists because preprocessing is linear but not free: a 1 MB body costs ~1.7 s across normalize, query-shape
|
|
42
|
-
* and the phrase grouper, and Node runs them on the one thread every other request is waiting on. A cap here is cheaper
|
|
43
|
-
* than fairness plumbing, and rejecting is more honest than accepting a body whose tail the parser will silently
|
|
44
|
-
* discard.
|
|
45
|
-
*/
|
|
46
|
-
export const MAX_ADDRESS_LENGTH = 1024
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* `POST /v1/parse` request body.
|
|
50
|
-
*/
|
|
51
|
-
export const ParseRequestSchema = z
|
|
52
|
-
.object({
|
|
53
|
-
address: z.string().max(MAX_ADDRESS_LENGTH),
|
|
54
|
-
debug: z.boolean().optional(),
|
|
55
|
-
input_mode: InputModeSchema.optional(),
|
|
56
|
-
})
|
|
57
|
-
.openapi("ParseRequest")
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* One `ParseOutcome.components` entry — mirrors {@linkcode ParseComponent} (`engine.ts`).
|
|
61
|
-
*/
|
|
62
|
-
export const ParseComponentSchema = z.object({ tag: z.string(), value: z.string() }).openapi("ParseComponent")
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* `POST /v1/parse` response — mirrors {@linkcode ParseOutcome} (`engine.ts`): the ordered components plus the full
|
|
66
|
-
* decoded tree. `tree` is the same loose-tree idiom {@link ResolveResponseSchema} uses (`api/schema.ts:134-146`) — the
|
|
67
|
-
* decoder's `AddressTree` is the engine's contract, not this wire schema's.
|
|
68
|
-
*/
|
|
69
|
-
export const ParseOutcomeSchema = z
|
|
70
|
-
.object({
|
|
71
|
-
input: z.string(),
|
|
72
|
-
components: z.array(ParseComponentSchema),
|
|
73
|
-
tree: z.looseObject({ roots: z.array(z.unknown()) }),
|
|
74
|
-
debug: z.string().optional(),
|
|
75
|
-
})
|
|
76
|
-
.openapi("ParseOutcome")
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* `POST /v1/geocode` request body.
|
|
80
|
-
*/
|
|
81
|
-
export const GeocodeRequestSchema = z
|
|
82
|
-
.object({
|
|
83
|
-
address: z.string().max(MAX_ADDRESS_LENGTH),
|
|
84
|
-
input_mode: InputModeSchema.optional(),
|
|
85
|
-
})
|
|
86
|
-
.openapi("GeocodeRequest")
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* One `GeocodeOutcome.hierarchy` entry — locality → country, most specific first. `name` is the resolved gazetteer name
|
|
90
|
-
* (proper-cased canonical); `value` is the raw parsed span. Mirrors `GeocodeResult["hierarchy"]` entries
|
|
91
|
-
* (`mailwoman/geocode-core.ts`), hand-modeled — see {@link GeocodeOutcomeSchema} for the no-import rationale.
|
|
92
|
-
*/
|
|
93
|
-
const GeocodeHierarchyEntrySchema = z
|
|
94
|
-
.object({
|
|
95
|
-
tag: z.string(),
|
|
96
|
-
value: z.string(),
|
|
97
|
-
name: z.string(),
|
|
98
|
-
lat: z.number().optional(),
|
|
99
|
-
lon: z.number().optional(),
|
|
100
|
-
placeID: z.string().optional(),
|
|
101
|
-
})
|
|
102
|
-
.openapi("GeocodeHierarchyEntry")
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* One `GeocodeOutcome.candidates` entry — a ranked alternative place for the query's primary result (the winning place
|
|
106
|
-
* first, then same-query runner-ups). Mirrors `GeocodeResult["candidates"]` entries.
|
|
107
|
-
*/
|
|
108
|
-
const GeocodeCandidateSchema = z
|
|
109
|
-
.object({
|
|
110
|
-
name: z.string(),
|
|
111
|
-
tag: z.string(),
|
|
112
|
-
lat: z.number(),
|
|
113
|
-
lon: z.number(),
|
|
114
|
-
countryCode: z.string().nullable(),
|
|
115
|
-
placeID: z.string().optional(),
|
|
116
|
-
})
|
|
117
|
-
.openapi("GeocodeCandidate")
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Canonical parsed-component map carried by `GeocodeResult.components`. Spelled out at this engine-agnostic API
|
|
121
|
-
* boundary for the same reason the result schema is hand-modeled; the compile-time drift pin in
|
|
122
|
-
* `mailwoman/test/api-schema-drift.test.ts` catches any mismatch with the real `ComponentTag`-keyed result type.
|
|
123
|
-
*/
|
|
124
|
-
const GeocodeComponentsSchema = z.partialRecord(
|
|
125
|
-
z.enum([
|
|
126
|
-
"country",
|
|
127
|
-
"region",
|
|
128
|
-
"locality",
|
|
129
|
-
"dependent_locality",
|
|
130
|
-
"postcode",
|
|
131
|
-
"subregion",
|
|
132
|
-
"house_number",
|
|
133
|
-
"street",
|
|
134
|
-
"street_prefix",
|
|
135
|
-
"street_prefix_particle",
|
|
136
|
-
"street_suffix",
|
|
137
|
-
"intersection_a",
|
|
138
|
-
"intersection_b",
|
|
139
|
-
"unit",
|
|
140
|
-
"venue",
|
|
141
|
-
"attention",
|
|
142
|
-
"po_box",
|
|
143
|
-
"cedex",
|
|
144
|
-
"prefecture",
|
|
145
|
-
"municipality",
|
|
146
|
-
"district",
|
|
147
|
-
"block",
|
|
148
|
-
"sub_block",
|
|
149
|
-
"building_number",
|
|
150
|
-
"building_name",
|
|
151
|
-
]),
|
|
152
|
-
z.string()
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* One `GeocodeOutcome.intent_markers` entry — an advisory the ROAD_TO_V9 §4 intent vocabulary raised about the QUERY.
|
|
157
|
-
* Mirrors `QueryIntentMarker` (`core/pipeline/types.ts`).
|
|
158
|
-
*
|
|
159
|
-
* `evidence` is deliberately open (`z.record`): each `code` carries its own measurement — a dominance margin, a pair of
|
|
160
|
-
* interpretations, a taxonomy id — and flattening those into one closed shape would either lose the numbers or invent
|
|
161
|
-
* fields that do not apply. `code` is the discriminator a client branches on.
|
|
162
|
-
*/
|
|
163
|
-
const QueryIntentMarkerSchema = z
|
|
164
|
-
.object({
|
|
165
|
-
// Spelled out rather than `z.string()` so `mailwoman/test/api-schema-drift.test.ts`'s schema-too-wide direction
|
|
166
|
-
// keeps biting: a new `QueryKind` that never reaches this list is a documented contract that has quietly stopped
|
|
167
|
-
// describing the real one.
|
|
168
|
-
kind: z.enum([
|
|
169
|
-
"postcode_only",
|
|
170
|
-
"locality_only",
|
|
171
|
-
"structured_address",
|
|
172
|
-
"intersection",
|
|
173
|
-
"po_box",
|
|
174
|
-
"landmark",
|
|
175
|
-
"poi_query",
|
|
176
|
-
"vague",
|
|
177
|
-
"bare_toponym",
|
|
178
|
-
"route_pair",
|
|
179
|
-
"near_me",
|
|
180
|
-
"poi_category",
|
|
181
|
-
]),
|
|
182
|
-
code: z.enum(["declared_ambiguity", "declared_fork", "focus_point_required", "poi_category"]),
|
|
183
|
-
mechanism: z.string(),
|
|
184
|
-
message: z.string(),
|
|
185
|
-
evidence: z.record(z.string(), z.unknown()).optional(),
|
|
186
|
-
})
|
|
187
|
-
.openapi("QueryIntentMarker")
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* `POST /v1/geocode` response — a hand-modeled mirror of `GeocodeResult`'s wire shape (`mailwoman/geocode-core.ts`),
|
|
191
|
-
* `.loose()` so a field the engine adds that this schema doesn't yet know about still rides through undocumented rather
|
|
192
|
-
* than being stripped or rejected. DOC-ACCURACY ONLY: the route passes `engine.geocode()`'s outcome through verbatim
|
|
193
|
-
* (`GeocodeOutcome = Record<string, unknown>`, `api/engine.ts`) — nothing here validates a real response, so a
|
|
194
|
-
* schema/engine mismatch can never reject or mutate a result at runtime. Deliberately carries NO import from
|
|
195
|
-
* `mailwoman` (the engine-agnosticism boundary — `mailwoman` is the one workspace allowed to depend on
|
|
196
|
-
* `@mailwoman/api`, never the reverse). `mailwoman/test/api-schema-drift.test.ts` is the compile-time tripwire that
|
|
197
|
-
* catches this shape drifting from the real `GeocodeResult` interface.
|
|
198
|
-
*/
|
|
199
|
-
export const GeocodeOutcomeSchema = z
|
|
200
|
-
.object({
|
|
201
|
-
input: z.string(),
|
|
202
|
-
components: GeocodeComponentsSchema,
|
|
203
|
-
lat: z.number().nullable(),
|
|
204
|
-
lon: z.number().nullable(),
|
|
205
|
-
resolution_tier: z.enum(["address_point", "interpolated", "street", "admin"]),
|
|
206
|
-
uncertainty_m: z.number().nullable(),
|
|
207
|
-
locality: z.string().nullable(),
|
|
208
|
-
region: z.string().nullable(),
|
|
209
|
-
postcode: z.string().nullable(),
|
|
210
|
-
house_number: z.string().nullable(),
|
|
211
|
-
street: z.string().nullable(),
|
|
212
|
-
// The parsed venue span (#1041 posture; surfaced 2026-08-01 for the hierarchy-evidence campaign R1).
|
|
213
|
-
venue: z.string().nullable(),
|
|
214
|
-
// The parsed dependent-locality span (parse view; `hierarchy` is the resolved view).
|
|
215
|
-
dependent_locality: z.string().nullable(),
|
|
216
|
-
// The parsed unit / sub-venue span (parse view) — "Terminal 5", "Suite 300".
|
|
217
|
-
unit: z.string().nullable(),
|
|
218
|
-
countryCode: z.string().nullable(),
|
|
219
|
-
hierarchy: z.array(GeocodeHierarchyEntrySchema),
|
|
220
|
-
candidates: z.array(GeocodeCandidateSchema),
|
|
221
|
-
// #42: the country the postcode-country coherence pass scoped the walk to, or null. Non-null ONLY when it
|
|
222
|
-
// OVERRODE the request's country prior — so a caller who asked for US and got an FR answer can see which
|
|
223
|
-
// evidence bought the change instead of reading it as a bug.
|
|
224
|
-
postcode_country_scope: z.string().nullable(),
|
|
225
|
-
// ROAD_TO_V9 §4: query-intent advisories. Always present; empty means the vocabulary looked and had nothing to
|
|
226
|
-
// say. Advisory ONLY — no marker changed which answer won, and a client is free to ignore the array entirely.
|
|
227
|
-
intent_markers: z.array(QueryIntentMarkerSchema),
|
|
228
|
-
})
|
|
229
|
-
.loose()
|
|
230
|
-
.openapi("GeocodeOutcome")
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* `POST /v1/batch` request body.
|
|
234
|
-
*/
|
|
235
|
-
export const BatchRequestSchema = z
|
|
236
|
-
.object({
|
|
237
|
-
// Per-ROW, not just per-request: the row cap (`batchMax`, default 500) bounds how many addresses arrive,
|
|
238
|
-
// and this bounds how large each may be. Without both, one request is 500 unbounded bodies.
|
|
239
|
-
addresses: z.array(z.string().max(MAX_ADDRESS_LENGTH)),
|
|
240
|
-
/**
|
|
241
|
-
* Register override for every row. DEFAULT `"formatted"` — batch rows are the record register by nature.
|
|
242
|
-
*/
|
|
243
|
-
input_mode: InputModeSchema.optional(),
|
|
244
|
-
})
|
|
245
|
-
.openapi("BatchRequest")
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* The failure slot for one batch row. A row that throws does not fail its neighbours.
|
|
249
|
-
*/
|
|
250
|
-
const BatchRowErrorSchema = z.object({ input: z.string(), error: z.string() })
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* One batch row: the geocode outcome, or the failure slot that stands in for it.
|
|
254
|
-
*/
|
|
255
|
-
const BatchRowSchema = z.union([GeocodeOutcomeSchema, BatchRowErrorSchema])
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* `POST /v1/batch` response — one `GeocodeOutcome`, or an `{ input, error }` slot, per row (per-row isolation).
|
|
259
|
-
*/
|
|
260
|
-
export const BatchResponseSchema = z
|
|
261
|
-
.object({
|
|
262
|
-
results: z.array(BatchRowSchema),
|
|
263
|
-
})
|
|
264
|
-
.openapi("BatchResponse")
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* `POST /v1/resolve` request body — an already-decoded `AddressTree` (the parser's output) to resolve against the
|
|
268
|
-
* gazetteer.
|
|
269
|
-
*/
|
|
270
|
-
export const ResolveRequestSchema = z
|
|
271
|
-
.object({
|
|
272
|
-
tree: z.looseObject({ roots: z.array(z.unknown()) }),
|
|
273
|
-
opts: z.looseObject({}).optional(),
|
|
274
|
-
})
|
|
275
|
-
.openapi("ResolveRequest")
|
|
276
|
-
|
|
277
|
-
/**
|
|
278
|
-
* `POST /v1/resolve` response — the same tree, decorated in place with gazetteer coords + attribution.
|
|
279
|
-
*/
|
|
280
|
-
export const ResolveResponseSchema = z
|
|
281
|
-
.object({
|
|
282
|
-
tree: z.looseObject({ roots: z.array(z.unknown()) }),
|
|
283
|
-
})
|
|
284
|
-
.openapi("ResolveResponse")
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* One component's value. Repeatable tags (a street with two names, say) arrive as an array; the caller joins them
|
|
288
|
-
* before handing the dict to `formatAddress`, which takes single strings only.
|
|
289
|
-
*/
|
|
290
|
-
const ComponentValueSchema = z.union([z.string(), z.array(z.string())])
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* `POST /v1/format` request body. `components` accepts `string | string[]` per key on the wire — a handler-side
|
|
294
|
-
* concern, not this schema's: `@mailwoman/formatter`'s `ComponentDict` (`format.ts`) is `Partial<Record<ComponentTag,
|
|
295
|
-
* string>>`, single-string only, so a route handler must join array values before calling
|
|
296
|
-
* `formatAddress`/`canonicalKey`.
|
|
297
|
-
*/
|
|
298
|
-
export const FormatRequestSchema = z
|
|
299
|
-
.object({
|
|
300
|
-
components: z.record(z.string(), ComponentValueSchema),
|
|
301
|
-
country: z.string(),
|
|
302
|
-
options: z.looseObject({}).optional(),
|
|
303
|
-
})
|
|
304
|
-
.openapi("FormatRequest")
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* `POST /v1/format` response — the rendered string plus the deterministic canonical match key.
|
|
308
|
-
*/
|
|
309
|
-
export const FormatResponseSchema = z
|
|
310
|
-
.object({
|
|
311
|
-
formatted: z.string(),
|
|
312
|
-
canonicalKey: z.string(),
|
|
313
|
-
})
|
|
314
|
-
.openapi("FormatResponse")
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* `GET /health` response — `status`/`uptime_s` are stamped by the ROUTE itself, unconditionally, regardless of engine
|
|
318
|
-
* (`api/routes.ts`'s `healthRoute` handler: `{ status: "ok", uptime_s, ...engine.health?.() }`), so those two are cheap
|
|
319
|
-
* + accurate to pin. Everything else is `HealthData` (`api/engine.ts`) — an engine-defined block (model card, data-root
|
|
320
|
-
* inventory for `mailwoman serve`; something else entirely for another engine) — stays loose.
|
|
321
|
-
*/
|
|
322
|
-
export const HealthResponseSchema = z
|
|
323
|
-
.object({
|
|
324
|
-
status: z.literal("ok"),
|
|
325
|
-
uptime_s: z.number(),
|
|
326
|
-
})
|
|
327
|
-
.loose()
|
|
328
|
-
.openapi("HealthResponse")
|