@mailwoman/kind-classifier 10.0.0 → 10.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/rules.ts CHANGED
@@ -3,66 +3,57 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * Rule-based classifiers for each `QueryKind`. Each rule inspects the normalized input + QueryShape
7
- * and returns a confidence score in [0, 1], or 0 if the rule doesn't fire.
8
- *
9
- * Bitter-lesson-safe: only universal structural patterns — no place-name dictionaries. ~1 small
10
- * regex set per new locale, not 50K dictionary entries.
6
+ * Rule-based `QueryKind` scorers. Each scorer reads structural patterns in the normalized input and
7
+ * its QueryShape and returns a confidence from 0 to 1.
11
8
  */
12
9
 
13
- import { NAME_PRONE_US_SUFFIXES, US_STREET_SUFFIX_LOOKUP } from "@mailwoman/codex/us/street-suffix"
10
+ import { NAME_PRONE_US_SUFFIXES, US_STREET_SUFFIX_LOOKUP } from "@mailwoman/codex/us/street/suffix"
14
11
  import type { NormalizedInputLite, QueryShapeSegmentsView as QueryShapeLike } from "@mailwoman/query-shape"
12
+ import { classifyTokens, foldInputClass } from "@mailwoman/query-shape/character-class"
15
13
  import { isPostcodeFormat } from "@mailwoman/query-shape/known-formats"
16
14
  /**
17
- * Longest input still plausible as a bare venue or landmark name. Beyond it the query is carrying an address as well,
18
- * and belongs to the structured-address scorer.
15
+ * Maximum length for a bare venue or landmark query.
19
16
  */
20
17
  const MAX_LANDMARK_LENGTH = 50
21
18
 
22
19
  /**
23
- * Longest input still plausible as a bare postcode, allowing for a country prefix and separators.
20
+ * Maximum length for a bare postcode, including an optional country prefix and separators.
24
21
  */
25
22
  const MAX_POSTCODE_ONLY_LENGTH = 16
26
23
 
27
24
  /**
28
- * Share of the input the postcode must occupy before `postcode_only` fires. Below it the query is carrying something
29
- * else too — a locality, a street — and another kind should win.
25
+ * Minimum fraction of the input that a postcode must cover for `postcode_only`.
30
26
  */
31
27
  const MIN_POSTCODE_COVERAGE = 0.7
32
28
 
33
29
  /**
34
- * Longest input still plausible as a bare locality name, including a trailing region code.
35
- *
36
- * Exported so `intent-rules.ts`'s `bare_toponym` can share the exact same ceiling. Sharing it is what makes
37
- * "bare_toponym is a strict refinement of locality_only" a structural property rather than two numbers that happen to
38
- * agree today.
30
+ * Maximum length for a bare locality.
31
+ * The `bare_toponym` intent rule uses the same limit.
39
32
  */
40
33
  export const MAX_LOCALITY_ONLY_LENGTH = 30
41
34
 
42
35
  /**
43
- * Word count of a short capitalized phrase — the shape of a venue name like `Empire State Building`. Wider than this
44
- * and the phrase is more likely a full address line.
36
+ * Minimum word count for a short venue phrase.
45
37
  */
46
38
  const VENUE_PHRASE_MIN_WORDS = 2
47
39
 
48
40
  /**
49
- * Upper bound of the short-phrase venue window; see {@link VENUE_PHRASE_MIN_WORDS}.
41
+ * Maximum word count for a short venue phrase.
50
42
  */
51
43
  const VENUE_PHRASE_MAX_WORDS = 4
52
44
 
53
45
  /**
54
- * Word count above which a single-segment proper-case phrase stops reading as a venue name.
46
+ * Maximum word count for a longer single-segment venue phrase.
55
47
  */
56
48
  const LONG_VENUE_PHRASE_MAX_WORDS = 6
57
49
 
58
50
  /**
59
- * Length at which a single-segment alphanumeric input reads as a full postcode rather than a fragment. Shorter
60
- * alphanumeric inputs score lower because they are as likely to be a unit number.
51
+ * Length at which single-segment alphanumeric input scores as a likely structured address.
61
52
  */
62
53
  const ALPHANUMERIC_POSTCODE_MIN_LENGTH = 15
63
54
 
64
55
  /**
65
- * Landmark vocabulary — phrases that suggest a vague-location description rather than an address.
56
+ * Phrases that signal a relative landmark description.
66
57
  */
67
58
  const LANDMARK_LEADERS = [
68
59
  "behind",
@@ -77,32 +68,30 @@ const LANDMARK_LEADERS = [
77
68
  ]
78
69
 
79
70
  /**
80
- * Intersection vocabulary — words that signal "where two streets cross" rather than an address.
71
+ * Patterns that signal a street intersection.
81
72
  */
82
73
  const INTERSECTION_PATTERNS = [
83
74
  /\bcorner of\b/i,
84
75
  /\bintersection of\b/i,
85
76
  /\bat the corner of\b/i,
86
- // "5th and Main", "Broadway & 42nd"
77
+ // Examples: "5th and Main", "Broadway & 42nd".
87
78
  /\b\w+(?:st|nd|rd|th|street|ave|avenue|blvd|boulevard|road|rd|lane|ln)?\s+(?:and|&|@)\s+\w+/i,
88
79
  ]
89
80
 
90
81
  /**
91
- * `po_box` rule: high-confidence iff QueryShape detected a po_box format hit. Confidence comes directly from the hit;
92
- * covers all locale variants (US "PO Box 123", FR "BP 42", etc.).
82
+ * Scores input in which QueryShape detected a PO Box format.
93
83
  */
94
84
  export function scorePoBox(_input: NormalizedInputLite, shape: QueryShapeLike): number {
95
85
  const hit = shape.knownFormats.find((f) => f.format === "po_box")
96
86
 
97
87
  if (!hit) return 0
98
88
 
99
- // Boost slightly above the raw hit confidence so po_box wins ties with structured_address when
100
- // both rules fire on the same input.
89
+ // The bonus makes a PO Box win a tie with a structured address.
101
90
  return Math.min(1, hit.confidence + 0.1)
102
91
  }
103
92
 
104
93
  /**
105
- * `intersection` rule: text matches one of the conventional intersection phrasings.
94
+ * Scores street-intersection phrasing.
106
95
  */
107
96
  export function scoreIntersection(input: NormalizedInputLite, _shape: QueryShapeLike): number {
108
97
  const text = input.normalized
@@ -115,8 +104,7 @@ export function scoreIntersection(input: NormalizedInputLite, _shape: QueryShape
115
104
  }
116
105
 
117
106
  /**
118
- * `landmark` rule: text begins with a landmark-leader phrase. These inputs are not addresses proper — they describe a
119
- * location relative to another place.
107
+ * Scores input that begins with a relative-landmark phrase.
120
108
  */
121
109
  export function scoreLandmark(input: NormalizedInputLite, _shape: QueryShapeLike): number {
122
110
  const lc = input.normalized.toLowerCase().trim()
@@ -129,19 +117,14 @@ export function scoreLandmark(input: NormalizedInputLite, _shape: QueryShapeLike
129
117
  }
130
118
 
131
119
  /**
132
- * Split on whitespace + commas and drop empty words — the word grammar shared with `intent-rules.ts`. Dropping empties
133
- * matters: a trailing comma or doubled separator otherwise yields an empty string that inflates the word count and
134
- * falsifies every-word predicates like the proper-case check below.
120
+ * Splits text on whitespace and commas and drops empty tokens.
135
121
  */
136
122
  export function wordsOf(text: string): string[] {
137
- return text.split(/[\s,]+/).filter((word) => word.length > 0)
123
+ return text.split(/[\s,]+/).filter((word) => word.length)
138
124
  }
139
125
 
140
126
  /**
141
- * True when a word is USPS street-suffix vocabulary that unambiguously signals an address — the full Pub-28 table via
142
- * `@mailwoman/codex`, minus its curated name-prone canonicals (PARK, FIELD, HILL, LAKE, …). Those double as ordinary
143
- * proper-name heads ("Wrigley Field", "Menlo Park"), and disqualifying on them would reject the very venue and place
144
- * names the rules below exist to capture. Shared with `intent-rules.ts` so both rule sets read one definition.
127
+ * Reports whether a word is a USPS street suffix that rarely appears in place names.
145
128
  */
146
129
  export function isDisqualifyingStreetSuffix(word: string): boolean {
147
130
  const canonical = US_STREET_SUFFIX_LOOKUP.get(word.trim().toLowerCase())
@@ -150,11 +133,80 @@ export function isDisqualifyingStreetSuffix(word: string): boolean {
150
133
  }
151
134
 
152
135
  /**
153
- * `landmark` rule (venue/named-place variant): short capitalized input with no street suffixes, no postcode hits, and
154
- * no region abbreviations. Captures "Pier 39", "Empire State Building", "Wrigley Field", "Grand Central Terminal".
136
+ * Removes postcode spans in the final segment and collapses the remaining separators.
137
+ */
138
+ export function withoutPostcodeSpans(text: string, shape: QueryShapeLike): string {
139
+ const merged = mergedPostcodeSpans(shape)
140
+
141
+ if (!merged.length) return text
142
+
143
+ let remainder = text
144
+
145
+ // The loop removes spans from last to first.
146
+ // This order keeps the remaining offsets valid.
147
+ for (let index = merged.length - 1; index >= 0; index--) {
148
+ const span = merged[index]!
149
+
150
+ remainder = remainder.slice(0, span.start) + remainder.slice(span.end)
151
+ }
152
+
153
+ return remainder
154
+ .replaceAll(/[\s,]+/gu, " ")
155
+ .replace(/[\s,]+$/u, "")
156
+ .trim()
157
+ }
158
+
159
+ /**
160
+ * Reports whether text contains a Unicode letter.
155
161
  *
156
- * Fires at moderate confidence (0.65) — below structured_address (0.9) so addresses always win, but above vague (0.3)
157
- * so the pipeline can route landmark queries to the venue resolver.
162
+ * By itself, the `alpha` character class can match punctuation-only input.
163
+ */
164
+ export function carriesLetter(text: string): boolean {
165
+ return /\p{L}/u.test(text)
166
+ }
167
+
168
+ /**
169
+ * Merges overlapping postcode hits in the final segment into sorted, disjoint spans.
170
+ */
171
+ function mergedPostcodeSpans(shape: QueryShapeLike): Array<{ start: number; end: number }> {
172
+ // Postcode hits in earlier segments are often house numbers.
173
+ const tail = shape.segments?.at(-1)?.span
174
+
175
+ if (!tail) return []
176
+
177
+ const merged: Array<{ start: number; end: number }> = []
178
+
179
+ for (const hit of shape.knownFormats) {
180
+ if (!isPostcodeFormat(hit.format)) continue
181
+
182
+ if (hit.span.start < tail.start || hit.span.end > tail.end) continue
183
+
184
+ merged.push({ start: hit.span.start, end: hit.span.end })
185
+ }
186
+
187
+ if (merged.length < 2) return merged
188
+
189
+ merged.sort((left, right) => left.start - right.start)
190
+
191
+ const disjoint: Array<{ start: number; end: number }> = [merged[0]!]
192
+
193
+ for (const span of merged.slice(1)) {
194
+ const last = disjoint.at(-1)!
195
+
196
+ if (span.start <= last.end) {
197
+ last.end = Math.max(last.end, span.end)
198
+
199
+ continue
200
+ }
201
+
202
+ disjoint.push(span)
203
+ }
204
+
205
+ return disjoint
206
+ }
207
+
208
+ /**
209
+ * Scores short capitalized venue names that have no street suffix or recognized format.
158
210
  */
159
211
  export function scoreVenueLandmark(input: NormalizedInputLite, shape: QueryShapeLike): number {
160
212
  const text = input.normalized.trim()
@@ -162,34 +214,28 @@ export function scoreVenueLandmark(input: NormalizedInputLite, shape: QueryShape
162
214
 
163
215
  if (len === 0 || len > MAX_LANDMARK_LENGTH) return 0
164
216
 
165
- // Must have at least one capitalized word.
166
217
  if (!/[A-Z]/.test(text)) return 0
167
218
 
168
- // Reject if any known postcode format hit exists.
169
219
  if (shape.knownFormats.length) return 0
170
220
 
171
- // Reject if it looks like a multi-segment structured address (City, ST ZIP).
172
221
  const segCount = shape.segments?.length ?? 1
173
222
 
174
223
  if (segCount > 2) return 0
175
224
 
176
- // Reject if any word is an unambiguous street suffix.
177
225
  const words = wordsOf(text)
178
226
 
179
227
  for (const w of words) {
180
228
  if (isDisqualifyingStreetSuffix(w)) return 0
181
229
  }
182
230
 
183
- // Reject if the first token is a pure number (house-number-leading pattern).
231
+ // A leading number usually is a house number.
184
232
  if (/^\d+\s/.test(text)) return 0
185
233
 
186
- // Boost if the input has a number NOT at the start (venue-style: "Pier 39", "Terminal 5").
234
+ // Venue names often contain a number after the first word.
187
235
  const hasInternalNumber = /\s\d+/.test(text) && !/^\d/.test(text)
188
236
 
189
- // Check if every word starts with uppercase (proper-noun pattern).
190
237
  const allProperCase = words.length > 1 && words.every((w) => /^[A-Z]/.test(w))
191
238
 
192
- // Boost for short single-segment capitalized phrases (2-4 words).
193
239
  const wordCount = words.length
194
240
 
195
241
  if (wordCount >= VENUE_PHRASE_MIN_WORDS && wordCount <= VENUE_PHRASE_MAX_WORDS && segCount === 1) {
@@ -200,7 +246,6 @@ export function scoreVenueLandmark(input: NormalizedInputLite, shape: QueryShape
200
246
  return 0.65
201
247
  }
202
248
 
203
- // Longer single-segment capitalized phrases get moderate confidence.
204
249
  if (wordCount <= LONG_VENUE_PHRASE_MAX_WORDS && segCount === 1 && allProperCase) {
205
250
  return 0.75
206
251
  }
@@ -209,10 +254,7 @@ export function scoreVenueLandmark(input: NormalizedInputLite, shape: QueryShape
209
254
  }
210
255
 
211
256
  /**
212
- * `postcode_only` rule: input is short AND has a postcode format hit covering most of it.
213
- *
214
- * The "covering most of it" check is what distinguishes `"10118"` (postcode-only) from `"350 5th Ave 10118"`
215
- * (structured-address with a postcode in it).
257
+ * Scores short input in which a postcode covers most of the text.
216
258
  */
217
259
  export function scorePostcodeOnly(input: NormalizedInputLite, shape: QueryShapeLike): number {
218
260
  const len = input.normalized.length
@@ -223,29 +265,49 @@ export function scorePostcodeOnly(input: NormalizedInputLite, shape: QueryShapeL
223
265
  if (!postcodeHit) return 0
224
266
  const hitLen = postcodeHit.span.end - postcodeHit.span.start
225
267
 
226
- // At least 70% of the input must be the postcode for the rule to fire confidently.
227
268
  if (hitLen / len < MIN_POSTCODE_COVERAGE) return 0
228
269
 
229
- // Confidence scales with how much of the input is the postcode and how confident the format hit was.
230
270
  return Math.min(1, postcodeHit.confidence * (hitLen / len) + 0.1)
231
271
  }
232
272
 
233
273
  /**
234
- * `locality_only` rule: short input, alpha-class, single segment, no format hits.
235
- *
236
- * Examples: `"Paris"`, `"NYC NY"`, `"Tokyo"`. Distinguishes from `structured_address` (multiple segments) and `vague`
237
- * (long or mixed-class).
274
+ * Scores a place name with at most an administrative tail and an optional postcode.
238
275
  */
239
276
  export function scoreLocalityOnly(input: NormalizedInputLite, shape: QueryShapeLike): number {
240
- const len = input.normalized.length
277
+ // Any recognized format other than a postcode rules out a bare locality.
278
+ let carriesPostcode = false
279
+
280
+ for (const hit of shape.knownFormats) {
281
+ if (isPostcodeFormat(hit.format)) {
282
+ carriesPostcode = true
283
+
284
+ continue
285
+ }
286
+
287
+ return 0
288
+ }
289
+
290
+ // Only postcode hits in the final segment can be removed.
291
+ const removable = carriesPostcode ? mergedPostcodeSpans(shape) : []
292
+
293
+ if (carriesPostcode && !removable.length) return 0
294
+
295
+ const withoutPostcode = carriesPostcode ? withoutPostcodeSpans(input.normalized, shape) : input.normalized
296
+ const len = withoutPostcode.length
241
297
 
242
298
  if (len === 0 || len > MAX_LOCALITY_ONLY_LENGTH) return 0
243
299
 
244
- if (shape.characterClass !== "alpha") return 0
300
+ if (!carriesLetter(withoutPostcode)) return 0
245
301
 
246
- if (shape.knownFormats.length) return 0
247
- // Locality-only inputs typically have 1-3 segments (e.g. "New York" is 1 segment, "Paris, FR" is 2).
248
- // We allow up to 2 segments before deciding it's structured.
302
+ // The remaining text must be alphabetic.
303
+ // Its class is recomputed only after a postcode was removed.
304
+ if (shape.characterClass !== "alpha") {
305
+ if (!carriesPostcode) return 0
306
+
307
+ if (foldInputClass(classifyTokens(withoutPostcode)) !== "alpha") return 0
308
+ }
309
+
310
+ // A locality and an administrative tail use at most two segments.
249
311
  const segCount = shape.segments?.length ?? 1
250
312
 
251
313
  if (segCount > 2) return 0
@@ -254,8 +316,7 @@ export function scoreLocalityOnly(input: NormalizedInputLite, shape: QueryShapeL
254
316
  }
255
317
 
256
318
  /**
257
- * `structured_address` rule: looks like a real multi-component address. Either has multiple segments or is long and
258
- * mixed-class.
319
+ * Scores multi-component address shapes.
259
320
  */
260
321
  export function scoreStructuredAddress(input: NormalizedInputLite, shape: QueryShapeLike): number {
261
322
  const len = input.normalized.length
@@ -263,26 +324,27 @@ export function scoreStructuredAddress(input: NormalizedInputLite, shape: QueryS
263
324
  if (len === 0) return 0
264
325
  const segCount = shape.segments?.length ?? 1
265
326
 
266
- // Multi-segment input with mixed character class = high confidence structured.
267
- if (segCount >= 2 && shape.characterClass === "alphanumeric") return 0.9
327
+ // Digits outside the postcode are required, so a locality with a postcode does not score here.
328
+ if (
329
+ segCount >= 2 &&
330
+ shape.characterClass === "alphanumeric" &&
331
+ /\d/u.test(withoutPostcodeSpans(input.normalized, shape))
332
+ ) {
333
+ return 0.9
334
+ }
268
335
 
269
- // Single-segment but reasonably long and alphanumeric = moderate confidence.
270
336
  if (len >= ALPHANUMERIC_POSTCODE_MIN_LENGTH && shape.characterClass === "alphanumeric") return 0.75
271
337
 
272
- // Multi-segment but pure-alpha = moderate (could be a multi-word locality).
338
+ // Multi-segment input without digits may still be a multi-word locality, so it scores lower.
273
339
  if (segCount >= 2) return 0.6
274
340
 
275
- // Single-segment, short, alphanumeric (e.g. "10118-1234" with no other content) — weak.
276
341
  if (len < ALPHANUMERIC_POSTCODE_MIN_LENGTH && shape.characterClass === "alphanumeric") return 0.4
277
342
 
278
343
  return 0
279
344
  }
280
345
 
281
346
  /**
282
- * `vague` rule: nothing else fired with high confidence — input is ambiguous.
283
- *
284
- * Returns a moderate baseline so `vague` always shows up as an alternative, even when other rules dominate. The
285
- * coordinator decides whether to trust vague as the primary kind.
347
+ * Returns a low constant fallback score for ambiguous input.
286
348
  */
287
349
  export function scoreVague(_input: NormalizedInputLite, _shape: QueryShapeLike): number {
288
350
  return 0.3
package/out/classify.d.ts CHANGED
@@ -3,30 +3,25 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * `classifyKind` — entry point for Stage 2.5 (kind classification).
6
+ * `classifyKind` is the entry point for Stage 2.5 (kind classification), composing the per-kind rules
7
+ * from `rules.ts` and `intent-rules.ts` and returning alternatives sorted by confidence.
7
8
  *
8
- * Composes the per-kind rules from `rules.ts` and `intent-rules.ts` and picks the winner. Returns
9
- * alternatives sorted by confidence so the coordinator can offer fallback paths when the top kind
10
- * isn't actionable.
11
- *
12
- * Per the project's "possibilities not constraints" principle, every kind that fires above 0
13
- * surfaces in `alternatives` — the caller decides whether to act on the top kind only or consider
14
- * runner-ups. The ROAD_TO_V9 §4 intent vocabulary leans on that: `bare_toponym` and `route_pair`
15
- * are scored below their structural incumbent precisely so they land in `alternatives`, where they
16
- * inform the markers without moving the routing decision.
9
+ * Per the project's "possibilities not constraints" principle, every kind that fires above 0 surfaces
10
+ * in `alternatives` and the caller decides whether to act on the top kind only: `bare_toponym` and
11
+ * `route_pair` are scored below their structural incumbent precisely so they land in `alternatives`,
12
+ * where they inform the markers without moving the routing decision.
17
13
  */
18
14
  import type { LocaleHint, QueryKindResult } from "@mailwoman/core/pipeline";
19
15
  import type { NormalizedInputLite, QueryShapeSegmentsView as QueryShapeLike } from "@mailwoman/query-shape";
20
16
  import { type POIPhraseLookup } from "#poi";
21
17
  /**
22
- * Classify the query shape into a `QueryKind`. Synchronous + pure — produces the same result for the same `(input,
23
- * shape)` pair.
18
+ * Classify the query shape into a `QueryKind`, synchronously and purely,
19
+ * producing the same result for the same `(input, shape)` pair.
24
20
  */
25
21
  export declare function classifyKindSync(input: NormalizedInputLite, shape: QueryShapeLike): QueryKindResult;
26
22
  /**
27
- * Async variant matching the runtime-pipeline's `classifyKind` contract.
28
- *
29
- * The locale parameter is accepted for future locale-aware rules (Japanese honorifics, etc.) but not currently used.
23
+ * Async variant matching the runtime pipeline's `classifyKind` interface;
24
+ * `_locale` is accepted for future locale-aware rules but currently unused.
30
25
  */
31
26
  export declare function classifyKind(input: NormalizedInputLite, shape: QueryShapeLike, _locale?: LocaleHint): Promise<QueryKindResult>;
32
27
  /**
@@ -34,15 +29,18 @@ export declare function classifyKind(input: NormalizedInputLite, shape: QuerySha
34
29
  */
35
30
  export interface KindClassifierOpts {
36
31
  /**
37
- * POI phrase lexicon (spec §3.1). When present, `poi_query` and `poi_category` scorers join the rule set — injected,
38
- * never imported, so this package stays dictionary-free. Absent → the returned classifier is behaviorally identical
39
- * to {@link classifyKind}.
32
+ * POI phrase lexicon (spec §3.1); when present the `poi_query`
33
+ * and `poi_category` scorers join the rule set.
34
+ *
35
+ * They are injected rather than imported so this package stays dictionary-free.
36
+ * When absent, the returned classifier behaves identically to {@link classifyKind}.
40
37
  */
41
38
  poiLexicon?: POIPhraseLookup;
42
39
  }
43
40
  /**
44
- * Build a kind classifier. Without opts this is exactly the default {@link classifyKind}; with a `poiLexicon` it
45
- * additionally scores `poi_query` + `poi_category` (ROAD_TO_V9 §4.4) and merges them into the ranked result.
41
+ * Build a kind classifier that is exactly {@link classifyKind} without options,
42
+ * or that additionally scores `poi_query` + `poi_category` (ROAD_TO_V9 §4.4)
43
+ * and merges them into the ranked result when given a `poiLexicon`.
46
44
  */
47
45
  export declare function createKindClassifier(opts?: KindClassifierOpts): (input: NormalizedInputLite, shape: QueryShapeLike, locale?: LocaleHint) => Promise<QueryKindResult>;
48
46
  //# sourceMappingURL=classify.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../lib/classify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAgC,eAAe,EAAE,MAAM,0BAA0B,CAAA;AACzG,OAAO,KAAK,EAAE,mBAAmB,EAAE,sBAAsB,IAAI,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAI3G,OAAO,EAA+C,KAAK,eAAe,EAAE,MAAM,MAAM,CAAA;AA4ExF;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,cAAc,GAAG,eAAe,CAMnG;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CACjC,KAAK,EAAE,mBAAmB,EAC1B,KAAK,EAAE,cAAc,EACrB,OAAO,CAAC,EAAE,UAAU,GAClB,OAAO,CAAC,eAAe,CAAC,CAE1B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC;;;;OAIG;IACH,UAAU,CAAC,EAAE,eAAe,CAAA;CAC5B;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CACnC,IAAI,GAAE,kBAAuB,GAC3B,CAAC,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,UAAU,KAAK,OAAO,CAAC,eAAe,CAAC,CA+BtG"}
1
+ {"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../lib/classify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAgC,eAAe,EAAE,MAAM,0BAA0B,CAAA;AACzG,OAAO,KAAK,EAAE,mBAAmB,EAAE,sBAAsB,IAAI,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAI3G,OAAO,EAA+C,KAAK,eAAe,EAAE,MAAM,MAAM,CAAA;AA4ExF;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,cAAc,GAAG,eAAe,CAMnG;AAED;;;GAGG;AACH,wBAAsB,YAAY,CACjC,KAAK,EAAE,mBAAmB,EAC1B,KAAK,EAAE,cAAc,EACrB,OAAO,CAAC,EAAE,UAAU,GAClB,OAAO,CAAC,eAAe,CAAC,CAE1B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAClC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,eAAe,CAAA;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CACnC,IAAI,GAAE,kBAAuB,GAC3B,CAAC,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,UAAU,KAAK,OAAO,CAAC,eAAe,CAAC,CAgCtG"}
package/out/classify.js CHANGED
@@ -3,17 +3,13 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * `classifyKind` — entry point for Stage 2.5 (kind classification).
6
+ * `classifyKind` is the entry point for Stage 2.5 (kind classification), composing the per-kind rules
7
+ * from `rules.ts` and `intent-rules.ts` and returning alternatives sorted by confidence.
7
8
  *
8
- * Composes the per-kind rules from `rules.ts` and `intent-rules.ts` and picks the winner. Returns
9
- * alternatives sorted by confidence so the coordinator can offer fallback paths when the top kind
10
- * isn't actionable.
11
- *
12
- * Per the project's "possibilities not constraints" principle, every kind that fires above 0
13
- * surfaces in `alternatives` — the caller decides whether to act on the top kind only or consider
14
- * runner-ups. The ROAD_TO_V9 §4 intent vocabulary leans on that: `bare_toponym` and `route_pair`
15
- * are scored below their structural incumbent precisely so they land in `alternatives`, where they
16
- * inform the markers without moving the routing decision.
9
+ * Per the project's "possibilities not constraints" principle, every kind that fires above 0 surfaces
10
+ * in `alternatives` and the caller decides whether to act on the top kind only: `bare_toponym` and
11
+ * `route_pair` are scored below their structural incumbent precisely so they land in `alternatives`,
12
+ * where they inform the markers without moving the routing decision.
17
13
  */
18
14
  import { deriveIntentMarkers } from "#intent/markers";
19
15
  import { scoreBareToponym, scoreNearMe, scoreRoutePair } from "#intent/rules";
@@ -26,17 +22,17 @@ const SCORERS = [
26
22
  { kind: "postcode_only", score: scorePostcodeOnly },
27
23
  { kind: "locality_only", score: scoreLocalityOnly },
28
24
  { kind: "structured_address", score: scoreStructuredAddress },
29
- // ROAD_TO_V9 §4. Ordinary members of the same list — intent is vocabulary, not a stage. `bare_toponym` and
30
- // `route_pair` are scored under `locality_only` on purpose (see `intent-rules.ts`), so their position here is
31
- // cosmetic; the sort below is what decides.
25
+ // Intent is vocabulary rather than a stage.
26
+ // The sort below decides, so `bare_toponym` and `route_pair` sit here cosmetically
27
+ // and are scored under `locality_only` on purpose (see `intent-rules.ts`).
32
28
  { kind: "bare_toponym", score: scoreBareToponym },
33
29
  { kind: "route_pair", score: scoreRoutePair },
34
30
  { kind: "near_me", score: scoreNearMe },
35
31
  { kind: "vague", score: scoreVague },
36
32
  ];
37
33
  /**
38
- * Rank a scored list and shape it into a verdict. Shared by the lexicon-free and lexicon-wired paths so the two cannot
39
- * drift in how they break ties or build `alternatives`.
34
+ * Rank a scored list into a verdict, shared by the lexicon-free and lexicon-wired paths
35
+ * so the two cannot drift in how they break ties or build `alternatives`.
40
36
  */
41
37
  function rank(scored) {
42
38
  scored.sort((a, b) => b.confidence - a.confidence);
@@ -48,17 +44,17 @@ function rank(scored) {
48
44
  };
49
45
  }
50
46
  /**
51
- * Every kind whose verdict carries `intentMarkers`. Checked before the marker builder runs so the hot path — a
52
- * structured address, where none of these fire — pays one set membership test per kind and nothing else.
47
+ * Every kind whose verdict includes `intentMarkers`, checked before the marker builder
48
+ * so the hot path pays one set membership test per kind.
53
49
  */
54
- const MARKER_BEARING_KINDS = new Set(["route_pair", "near_me", "poi_category"]);
50
+ const MARKER_KINDS = new Set(["route_pair", "near_me", "poi_category"]);
55
51
  /**
56
- * Attach markers to a verdict, or return it untouched. Separate from {@link rank} because the lexicon-wired path needs
57
- * to merge `poi_query`/`poi_category` in first.
52
+ * Attach markers to a verdict or return it untouched, separate from {@link rank}
53
+ * because the lexicon-wired path merges `poi_query`/`poi_category` in first.
58
54
  */
59
55
  function withIntentMarkers(verdict, input, poiLexicon, locale) {
60
56
  const kinds = [{ kind: verdict.kind, confidence: verdict.confidence }, ...verdict.alternatives];
61
- if (!kinds.some((k) => MARKER_BEARING_KINDS.has(k.kind)))
57
+ if (!kinds.some((k) => MARKER_KINDS.has(k.kind)))
62
58
  return verdict;
63
59
  const intentMarkers = deriveIntentMarkers(kinds, { input, poiLexicon, locale });
64
60
  if (!intentMarkers.length)
@@ -66,24 +62,24 @@ function withIntentMarkers(verdict, input, poiLexicon, locale) {
66
62
  return { ...verdict, intentMarkers };
67
63
  }
68
64
  /**
69
- * Classify the query shape into a `QueryKind`. Synchronous + pure — produces the same result for the same `(input,
70
- * shape)` pair.
65
+ * Classify the query shape into a `QueryKind`, synchronously and purely,
66
+ * producing the same result for the same `(input, shape)` pair.
71
67
  */
72
68
  export function classifyKindSync(input, shape) {
73
69
  const scored = SCORERS.map((s) => ({ kind: s.kind, confidence: s.score(input, shape) })).filter((s) => s.confidence > 0);
74
70
  return withIntentMarkers(rank(scored), input);
75
71
  }
76
72
  /**
77
- * Async variant matching the runtime-pipeline's `classifyKind` contract.
78
- *
79
- * The locale parameter is accepted for future locale-aware rules (Japanese honorifics, etc.) but not currently used.
73
+ * Async variant matching the runtime pipeline's `classifyKind` interface;
74
+ * `_locale` is accepted for future locale-aware rules but currently unused.
80
75
  */
81
76
  export async function classifyKind(input, shape, _locale) {
82
77
  return classifyKindSync(input, shape);
83
78
  }
84
79
  /**
85
- * Build a kind classifier. Without opts this is exactly the default {@link classifyKind}; with a `poiLexicon` it
86
- * additionally scores `poi_query` + `poi_category` (ROAD_TO_V9 §4.4) and merges them into the ranked result.
80
+ * Build a kind classifier that is exactly {@link classifyKind} without options,
81
+ * or that additionally scores `poi_query` + `poi_category` (ROAD_TO_V9 §4.4)
82
+ * and merges them into the ranked result when given a `poiLexicon`.
87
83
  */
88
84
  export function createKindClassifier(opts = {}) {
89
85
  const { poiLexicon } = opts;
@@ -96,9 +92,10 @@ export function createKindClassifier(opts = {}) {
96
92
  const categoryConfidence = createScorePOICategory(poiLexicon, localeTag)(input, shape);
97
93
  if (poiConfidence <= 0 && categoryConfidence <= 0)
98
94
  return base;
99
- // Re-rank over the union rather than special-casing "did POI beat the base?". The base verdict's own
100
- // alternatives are preserved, which is what keeps `bare_toponym` / `route_pair` visible to the marker builder
101
- // even when a POI kind takes the top slot.
95
+ // Re-ranking over the union rather than special-casing whether POI beat the
96
+ // base preserves the base's own alternatives.
97
+ // Those keep `bare_toponym`/`route_pair` visible to the marker builder even
98
+ // when a POI kind takes the top slot.
102
99
  const merged = [
103
100
  { kind: base.kind, confidence: base.confidence },
104
101
  ...base.alternatives,
@@ -1 +1 @@
1
- {"version":3,"file":"classify.js","sourceRoot":"","sources":["../lib/classify.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"classify.js","sourceRoot":"","sources":["../lib/classify.ts"],"names":[],"mappings":"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"}
package/out/index.d.ts CHANGED
@@ -3,14 +3,7 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  *
6
- * `@mailwoman/kind-classifier` — Stage 2.5 of the runtime pipeline.
7
- *
8
- * Categorize inputs into one of eight `QueryKind`s by composing rule-based scorers over the
9
- * QueryShape sub-system's output. Pure functions, no ML, no place-name dictionaries. Returns
10
- * possibilities (alternatives) alongside the top pick so the coordinator can fall back when the
11
- * winning kind isn't actionable.
12
- *
13
- * See `docs/engineering/reference/STAGES.md` § Stage 2.5 for the contract.
6
+ * Stage 2.5 classifier: compose rule-based scorers over QueryShape and return a top kind with alternatives.
14
7
  */
15
8
  export { classifyKind, classifyKindSync, createKindClassifier } from "#classify";
16
9
  export type { KindClassifierOpts } from "#classify";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAChF,YAAY,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AACrD,YAAY,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAC1D,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC5F,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,MAAM,CAAA;AACxD,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,YAAY,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,MAAM,CAAA;AAE9G,OAAO,EACN,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,UAAU,EACV,iBAAiB,EACjB,sBAAsB,EACtB,UAAU,EACV,kBAAkB,GAClB,MAAM,QAAQ,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../lib/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAChF,YAAY,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAA;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AACrD,YAAY,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAC1D,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAC5F,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,MAAM,CAAA;AACxD,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,YAAY,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,MAAM,CAAA;AAE9G,OAAO,EACN,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,UAAU,EACV,iBAAiB,EACjB,sBAAsB,EACtB,UAAU,EACV,kBAAkB,GAClB,MAAM,QAAQ,CAAA"}