@whiteslove/parsing-lexicon 0.9.0 → 0.9.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whiteslove/parsing-lexicon",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Shared deterministic multilingual parsing lexicon for WhitesLove housing and hiring services",
5
5
  "repository": {
6
6
  "type": "git",
@@ -197,6 +197,10 @@
197
197
  "types": "./src/semantic-spans.d.ts",
198
198
  "import": "./src/semantic-spans.js"
199
199
  },
200
+ "./address-grammar": {
201
+ "types": "./src/address-grammar.d.ts",
202
+ "import": "./src/address-grammar.js"
203
+ },
200
204
  "./hiring-professions": "./src/hiring-professions.js",
201
205
  "./hiring-profession-display": {
202
206
  "types": "./src/hiring-profession-display.d.ts",
@@ -0,0 +1,13 @@
1
+ export type AddressGrammarLanguage = 'ru' | 'uk' | 'ro' | 'en' | 'kk' | 'ky' | 'uzLatn' | 'uzCyrl';
2
+ export type AddressGrammarMarker = Readonly<{ pattern: string; lang: AddressGrammarLanguage }>;
3
+
4
+ export const STREET_PREFIX_MARKERS: readonly AddressGrammarMarker[];
5
+ export const STREET_POSTFIX_MARKERS: readonly AddressGrammarMarker[];
6
+ export const STREET_TYPE_MARKERS: readonly AddressGrammarMarker[];
7
+ export const HOUSE_MARKERS: readonly AddressGrammarMarker[];
8
+ export const BUILDING_MARKERS: readonly AddressGrammarMarker[];
9
+
10
+ export const COUNTRY_ADDRESS_LANGUAGES: Readonly<Record<string, readonly AddressGrammarLanguage[]>>;
11
+
12
+ export function combinedMarkerPattern(markers: readonly AddressGrammarMarker[]): string;
13
+ export function matchesCountryAddressLanguage(text: unknown, country: unknown, markers: readonly AddressGrammarMarker[]): boolean;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Country/language-scoped address grammar data, extracted from
3
+ * housing-address.js's combined marker regexes. This is data, not logic:
4
+ * housing-address.js still runs one shared parser engine and still builds
5
+ * the exact same combined patterns from these fragments (verified
6
+ * byte-identical to the pre-extraction hardcoded regex source in
7
+ * address-grammar.test.js) — nothing about matching behavior changes.
8
+ *
9
+ * The value of splitting it out is making "which markers belong to which
10
+ * language/country" an explicit, inspectable data structure instead of
11
+ * only implicit in one long alternation, so a caller that already knows
12
+ * the listing's country can use STREET_GRAMMAR_LANGUAGES_BY_COUNTRY as
13
+ * scoring evidence (e.g. "this address's markers are consistent with the
14
+ * supplied country") — a prior, per the plan, never a rigid requirement,
15
+ * since messy marketplace text routinely mixes languages regardless of
16
+ * country.
17
+ */
18
+
19
+ // Each marker fragment is tagged by the language it belongs to (not the
20
+ // country) because that's what's actually true of the text — "ул./улица"
21
+ // is a Russian-language marker that shows up in UZ, KZ, KG and UA listings
22
+ // alike, not a marker exclusive to one country.
23
+ export const STREET_PREFIX_MARKERS = Object.freeze([
24
+ { pattern: 'ул(?:ица)?', lang: 'ru' },
25
+ { pattern: 'вул(?:иця)?', lang: 'uk' },
26
+ { pattern: 'пр', lang: 'ru' },
27
+ { pattern: 'просп(?:ект)?', lang: 'ru' },
28
+ { pattern: 'пр-т', lang: 'ru' },
29
+ { pattern: 'переул(?:ок)?', lang: 'ru' },
30
+ { pattern: 'пров(?:улок)?', lang: 'uk' },
31
+ { pattern: 'проезд', lang: 'ru' },
32
+ { pattern: 'наб(?:ережная)?', lang: 'ru' },
33
+ { pattern: 'шоссе', lang: 'ru' },
34
+ { pattern: "str(?:ada)?", lang: 'ro' },
35
+ { pattern: 'street', lang: 'en' },
36
+ { pattern: 'st', lang: 'en' },
37
+ { pattern: 'avenue', lang: 'en' },
38
+ { pattern: 'ave', lang: 'en' },
39
+ { pattern: 'road', lang: 'en' },
40
+ { pattern: 'rd', lang: 'en' },
41
+ { pattern: 'көше', lang: 'kk' },
42
+ ]);
43
+
44
+ export const STREET_POSTFIX_MARKERS = Object.freeze([
45
+ { pattern: 'ko[\'’ʼ\\u02bc]?cha(?:si)?', lang: 'uzLatn' },
46
+ { pattern: 'кўча(?:си)?', lang: 'uzCyrl' },
47
+ { pattern: 'коча(?:си)?', lang: 'uzCyrl' },
48
+ { pattern: 'kocha(?:si)?', lang: 'uzLatn' },
49
+ { pattern: 'көше(?:сі)?', lang: 'kk' },
50
+ { pattern: 'көшесі', lang: 'kk' },
51
+ { pattern: 'көчө(?:сү)?', lang: 'ky' },
52
+ ]);
53
+
54
+ export const STREET_TYPE_MARKERS = Object.freeze([
55
+ { pattern: 'вулиця', lang: 'uk' },
56
+ { pattern: 'улица', lang: 'ru' },
57
+ { pattern: 'провулок', lang: 'uk' },
58
+ { pattern: 'переулок', lang: 'ru' },
59
+ { pattern: 'проспект', lang: 'ru' },
60
+ { pattern: 'бульвар', lang: 'ru' },
61
+ { pattern: 'набережна', lang: 'uk' },
62
+ { pattern: 'набережная', lang: 'ru' },
63
+ { pattern: 'шосе', lang: 'uk' },
64
+ { pattern: 'шоссе', lang: 'ru' },
65
+ { pattern: 'площа', lang: 'uk' },
66
+ { pattern: 'площадь', lang: 'ru' },
67
+ { pattern: 'узвіз', lang: 'uk' },
68
+ { pattern: 'спуск', lang: 'ru' },
69
+ { pattern: 'алея', lang: 'uk' },
70
+ { pattern: 'аллея', lang: 'ru' },
71
+ { pattern: 'дорога', lang: 'ru' },
72
+ { pattern: 'тупик', lang: 'ru' },
73
+ { pattern: 'көше(?:сі)?', lang: 'kk' },
74
+ { pattern: 'көшесі', lang: 'kk' },
75
+ { pattern: 'көчө(?:сү)?', lang: 'ky' },
76
+ ]);
77
+
78
+ export const HOUSE_MARKERS = Object.freeze([
79
+ { pattern: 'дом', lang: 'ru' },
80
+ { pattern: 'д\\.', lang: 'ru' },
81
+ { pattern: 'будинок', lang: 'uk' },
82
+ { pattern: 'буд\\.', lang: 'uk' },
83
+ { pattern: 'house', lang: 'en' },
84
+ { pattern: 'h\\.', lang: 'en' },
85
+ { pattern: 'uy', lang: 'uzLatn' },
86
+ { pattern: 'уй', lang: 'uzCyrl' },
87
+ { pattern: 'үй', lang: 'kk' },
88
+ { pattern: 'nr\\.?', lang: 'ro' },
89
+ { pattern: 'no\\.?', lang: 'en' },
90
+ { pattern: '№', lang: 'ru' },
91
+ ]);
92
+
93
+ export const BUILDING_MARKERS = Object.freeze([
94
+ { pattern: 'корп(?:ус)?\\.?', lang: 'ru' },
95
+ { pattern: 'к\\.', lang: 'ru' },
96
+ { pattern: 'строен(?:ие)?', lang: 'ru' },
97
+ { pattern: 'стр\\.', lang: 'ru' },
98
+ { pattern: 'будова', lang: 'uk' },
99
+ { pattern: 'секц(?:ия|ія)?', lang: 'ru' },
100
+ { pattern: 'bloc', lang: 'ro' },
101
+ { pattern: 'corp', lang: 'ro' },
102
+ { pattern: 'building', lang: 'en' },
103
+ { pattern: 'bldg\\.?', lang: 'en' },
104
+ { pattern: 'korpus', lang: 'uzLatn' },
105
+ { pattern: 'bino', lang: 'uzLatn' },
106
+ { pattern: 'bina', lang: 'uzLatn' },
107
+ { pattern: 'бино', lang: 'uzCyrl' },
108
+ ]);
109
+
110
+ // Which languages a given priority country's listings commonly mix, in the
111
+ // order they're worth checking first. Deliberately not exhaustive or
112
+ // exclusive — messy marketplace text can and does use any language
113
+ // regardless of country, so this is scoring-prior data, not a filter.
114
+ export const COUNTRY_ADDRESS_LANGUAGES = Object.freeze({
115
+ UZ: Object.freeze(['uzLatn', 'uzCyrl', 'ru']),
116
+ KZ: Object.freeze(['kk', 'ru']),
117
+ KG: Object.freeze(['ky', 'ru']),
118
+ UA: Object.freeze(['uk', 'ru']),
119
+ RO: Object.freeze(['ro']),
120
+ });
121
+
122
+ function joinMarkerGroup(markers) {
123
+ return markers.map((marker) => marker.pattern).join('|');
124
+ }
125
+
126
+ /** Rebuild the exact combined alternation source housing-address.js uses internally, from the tagged marker data above. */
127
+ export function combinedMarkerPattern(markers) {
128
+ return joinMarkerGroup(markers);
129
+ }
130
+
131
+ /** True when `text` contains at least one marker tagged for one of `country`'s priority languages. */
132
+ export function matchesCountryAddressLanguage(text, country, markers) {
133
+ const languages = COUNTRY_ADDRESS_LANGUAGES[String(country || '').toUpperCase()];
134
+ if (!languages?.length) return false;
135
+ const relevant = markers.filter((marker) => languages.includes(marker.lang));
136
+ if (!relevant.length) return false;
137
+ const pattern = new RegExp(`(?:${joinMarkerGroup(relevant)})`, 'iu');
138
+ return pattern.test(String(text || ''));
139
+ }
@@ -5,14 +5,28 @@ import {
5
5
  matchTashkentNumberedArea,
6
6
  } from './tashkent-housing-geography.js';
7
7
  import { detectNonAddressSpans } from './semantic-spans.js';
8
+ import {
9
+ BUILDING_MARKERS,
10
+ HOUSE_MARKERS,
11
+ STREET_POSTFIX_MARKERS,
12
+ STREET_PREFIX_MARKERS,
13
+ STREET_TYPE_MARKERS,
14
+ combinedMarkerPattern,
15
+ } from './address-grammar.js';
8
16
 
9
17
  const PHONE_RUN_RE = /\+?\d[\d\s().-]{7,}\d/gu;
10
18
  const ADDRESS_LABEL_RE = /(?:адрес|адреса|адресація|адресация|manzil|address|adresă|adresa)\s*[:=\-–—]\s*/iu;
11
- const PREFIX_STREET_MARKER = String.raw`(?:(?:ул(?:ица)?|вул(?:иця)?|пр|просп(?:ект)?|пр-т|переул(?:ок)?|пров(?:улок)?|проезд|наб(?:ережная)?|шоссе|str(?:ada)?|street|st|avenue|ave|road|rd|көше)\.?)`;
12
- const POSTFIX_STREET_MARKER = String.raw`(?:ko['’ʼ\u02bc]?cha(?:si)?|кўча(?:си)?|коча(?:си)?|kocha(?:si)?|көше(?:сі)?|көшесі|көчө(?:сү)?)`;
13
- const POSTFIX_STREET_TYPE = String.raw`(?:вулиця|улица|провулок|переулок|проспект|бульвар|набережна|набережная|шосе|шоссе|площа|площадь|узвіз|спуск|алея|аллея|дорога|тупик|көше(?:сі)?|көшесі|көчө(?:сү)?)`;
14
- const HOUSE_MARKER = String.raw`(?:дом|д\.|будинок|буд\.|house|h\.|uy|уй|үй|nr\.?|no\.?|№)`;
15
- const BUILDING_MARKER = String.raw`(?:корп(?:ус)?\.?|к\.|строен(?:ие)?|стр\.|будова|секц(?:ия|ія)?|bloc|corp|building|bldg\.?|korpus|bino|bina|бино)`;
19
+ // These marker alternations are built from address-grammar.js's tagged,
20
+ // per-language fragment data (one shared parser engine, country/language
21
+ // grammar as data — see that file) rather than hardcoded here. The
22
+ // combined pattern strings are verified byte-identical to the original
23
+ // hardcoded regexes in address-grammar.test.js, so this is a pure data
24
+ // extraction with no behavior change.
25
+ const PREFIX_STREET_MARKER = String.raw`(?:(?:${combinedMarkerPattern(STREET_PREFIX_MARKERS)})\.?)`;
26
+ const POSTFIX_STREET_MARKER = `(?:${combinedMarkerPattern(STREET_POSTFIX_MARKERS)})`;
27
+ const POSTFIX_STREET_TYPE = `(?:${combinedMarkerPattern(STREET_TYPE_MARKERS)})`;
28
+ const HOUSE_MARKER = `(?:${combinedMarkerPattern(HOUSE_MARKERS)})`;
29
+ const BUILDING_MARKER = `(?:${combinedMarkerPattern(BUILDING_MARKERS)})`;
16
30
  const NUMBER_TOKEN = String.raw`\d{1,5}(?:[-\/]?[\p{L}]\d{0,4})?(?:[\/-]\d{1,4}(?:[-\/]?[\p{L}]\d{0,4})?){0,2}`;
17
31
  const STREET_WORD = String.raw`[\p{L}'’.-]{2,48}`;
18
32
  // Common post-Soviet street names lead with a bare numeral ("8 Марта",