@whiteslove/parsing-lexicon 0.9.0 → 0.9.3
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 +5 -1
- package/src/address-grammar.d.ts +13 -0
- package/src/address-grammar.js +139 -0
- package/src/central-asia-locations.js +16 -4
- package/src/housing-address.js +19 -5
- package/src/kg-map-data-location-extensions.js +3 -0
- package/src/kz-map-data-location-extensions.js +3 -0
- package/src/location-merge.js +42 -2
- package/src/locations-runtime.js +24 -4
- package/src/locations.js +73 -3
- package/src/ro-map-data-location-extensions.js +3 -0
- package/src/ua-map-data-location-extensions.js +3 -0
- package/src/uz-map-data-location-extensions.js +1 -74375
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whiteslove/parsing-lexicon",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
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
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LOCATION_DICTIONARIES } from './locations-runtime.js';
|
|
2
|
-
import { LOCATION_LIST_KEYS } from './location-merge.js';
|
|
2
|
+
import { isMapDataEntry, LOCATION_LIST_KEYS } from './location-merge.js';
|
|
3
3
|
import { CITIES_BY_COUNTRY, canonicalCity } from './geography.js';
|
|
4
4
|
import { aliasesOf, aliasesToRegex, normalizeForMatch } from './normalization.js';
|
|
5
5
|
import { KZ_AMBIGUOUS_LOCAL_NAMES, KZ_SEARCH_CLUSTERS } from './kz-location-extensions.js';
|
|
@@ -227,12 +227,24 @@ function publicMatch(candidate) {
|
|
|
227
227
|
return Object.freeze(result);
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
function
|
|
230
|
+
function mapDataMatch(value, normalizedValue, item) {
|
|
231
|
+
const paddedValue = ` ${normalizedValue} `;
|
|
232
|
+
for (const alias of item.aliases || []) {
|
|
233
|
+
const normalizedAlias = normalizeForMatch(alias);
|
|
234
|
+
const index = paddedValue.indexOf(` ${normalizedAlias} `);
|
|
235
|
+
if (normalizedAlias && index >= 0) return { 0: normalizedAlias, index };
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function findEntryMatches(text, cityName, data, { includeMapData = false } = {}) {
|
|
231
241
|
const value = String(text || '');
|
|
242
|
+
const normalizedValue = normalizeForMatch(value);
|
|
232
243
|
const raw = [];
|
|
233
244
|
for (const key of LOCATION_LIST_KEYS) {
|
|
234
245
|
for (const item of data?.[key] || []) {
|
|
235
|
-
|
|
246
|
+
if (isMapDataEntry(item) && !includeMapData) continue;
|
|
247
|
+
const match = (isMapDataEntry(item) ? mapDataMatch(value, normalizedValue, item) : value.match(item?.re))
|
|
236
248
|
|| (key === 'residentialComplexes' ? markedResidentialMatch(value, item) : null);
|
|
237
249
|
if (!match) continue;
|
|
238
250
|
const { start, end } = semanticBounds(value, match);
|
|
@@ -319,7 +331,7 @@ export function matchCentralAsiaLocationEntities(text, countryCode, preferredCit
|
|
|
319
331
|
const scopedCity = preferred && country[preferred] ? preferred : explicit && country[explicit] ? explicit : null;
|
|
320
332
|
|
|
321
333
|
if (scopedCity) {
|
|
322
|
-
const matches = findEntryMatches(text, scopedCity, country[scopedCity]);
|
|
334
|
+
const matches = findEntryMatches(text, scopedCity, country[scopedCity], { includeMapData: true });
|
|
323
335
|
const clusters = clusterMatches(matches, countryCode);
|
|
324
336
|
return Object.freeze({ city: scopedCity, matches: Object.freeze(matches), searchClusters: Object.freeze(clusters), candidates: Object.freeze([]) });
|
|
325
337
|
}
|
package/src/housing-address.js
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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 Марта",
|