@whiteslove/parsing-lexicon 0.2.0 → 0.2.2
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/LICENSE +21 -21
- package/README.md +137 -137
- package/index.d.ts +374 -374
- package/package.json +77 -75
- package/src/central-asia-locations.js +211 -211
- package/src/central-asia.js +233 -233
- package/src/contact.d.ts +44 -44
- package/src/contact.js +159 -159
- package/src/countries.js +175 -175
- package/src/country-context.d.ts +10 -10
- package/src/country-context.js +22 -22
- package/src/currency.d.ts +14 -14
- package/src/currency.js +66 -66
- package/src/geo.js +169 -169
- package/src/geography-central-asia.js +42 -42
- package/src/geography-detection.d.ts +14 -14
- package/src/geography-detection.js +64 -64
- package/src/geography-display.d.ts +13 -13
- package/src/geography-display.js +113 -113
- package/src/geography.js +210 -210
- package/src/hiring-advanced.js +147 -147
- package/src/hiring-candidate-fields.d.ts +9 -9
- package/src/hiring-candidate-fields.js +92 -92
- package/src/hiring-context.d.ts +64 -64
- package/src/hiring-context.js +412 -412
- package/src/hiring-language-extensions.d.ts +19 -19
- package/src/hiring-language-extensions.js +139 -139
- package/src/hiring-languages.js +135 -135
- package/src/hiring-location-fields.d.ts +3 -3
- package/src/hiring-location-fields.js +53 -53
- package/src/hiring-professions-ro.js +191 -191
- package/src/hiring-professions.js +437 -437
- package/src/hiring-requirements.d.ts +16 -16
- package/src/hiring-requirements.js +170 -170
- package/src/hiring-salary-context.d.ts +27 -27
- package/src/hiring-salary-context.js +76 -76
- package/src/hiring-semantics.d.ts +29 -29
- package/src/hiring-semantics.js +218 -218
- package/src/hiring-skills.d.ts +21 -21
- package/src/hiring-skills.js +315 -315
- package/src/hiring-source-aliases.d.ts +32 -32
- package/src/hiring-source-aliases.js +185 -185
- package/src/hiring-source-semantics.d.ts +31 -31
- package/src/hiring-source-semantics.js +293 -293
- package/src/hiring-temporal.d.ts +15 -15
- package/src/hiring-temporal.js +67 -67
- package/src/hiring-vacancy-fields.d.ts +5 -5
- package/src/hiring-vacancy-fields.js +23 -23
- package/src/hiring-work-semantics.d.ts +34 -34
- package/src/hiring-work-semantics.js +72 -72
- package/src/hiring.js +180 -180
- package/src/housing-address.d.ts +18 -18
- package/src/housing-address.js +217 -210
- package/src/housing-commercial.d.ts +1 -0
- package/src/housing-commercial.js +9 -0
- package/src/housing-context.d.ts +36 -36
- package/src/housing-context.js +183 -183
- package/src/housing-features.js +30 -30
- package/src/housing-intent.d.ts +9 -9
- package/src/housing-intent.js +132 -132
- package/src/housing-listing-fields.js +131 -131
- package/src/housing-money.d.ts +7 -7
- package/src/housing-money.js +102 -102
- package/src/housing-source-aliases.d.ts +8 -8
- package/src/housing-source-aliases.js +51 -51
- package/src/housing-structured.d.ts +55 -55
- package/src/housing-structured.js +219 -219
- package/src/housing.js +200 -200
- package/src/index.js +45 -44
- package/src/kz-location-extensions.js +307 -307
- package/src/landmarks.js +21 -21
- package/src/lexicon-core.js +58 -58
- package/src/location-merge.js +106 -106
- package/src/locations.js +456 -456
- package/src/money-core.d.ts +10 -10
- package/src/money-core.js +81 -81
- package/src/money-lexicon.d.ts +5 -5
- package/src/money-lexicon.js +79 -79
- package/src/money.js +154 -154
- package/src/normalization.js +384 -384
- package/src/odesa-metropolitan.js +144 -144
- package/src/romania-geography.js +47 -47
- package/src/tashkent-colloquial.js +80 -72
- package/src/tashkent-housing-geography.d.ts +38 -38
- package/src/tashkent-housing-geography.js +211 -211
- package/src/tashkent-pois.js +215 -215
- package/src/tashkent-residential-complexes.js +272 -272
- package/src/ua-location-extensions-major.js +118 -118
- package/src/ua-location-extensions-metro.js +9 -9
- package/src/ua-location-extensions-regional.js +246 -246
- package/src/ua-secondary-cities.d.ts +2 -2
- package/src/ua-secondary-cities.js +57 -57
- package/src/ukraine.js +75 -75
- package/src/uz-location-extensions.js +259 -259
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
export interface TashkentHousingLocationEntry {
|
|
2
|
-
readonly canonical: string;
|
|
3
|
-
readonly name: string;
|
|
4
|
-
readonly category: string;
|
|
5
|
-
readonly country: 'UZ';
|
|
6
|
-
readonly city: 'Tashkent';
|
|
7
|
-
readonly aliases: readonly string[];
|
|
8
|
-
readonly re: RegExp;
|
|
9
|
-
readonly contextRequired: boolean;
|
|
10
|
-
readonly contextRe: RegExp | null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface TashkentNumberedAreaMatch {
|
|
14
|
-
readonly number: number;
|
|
15
|
-
readonly suffix: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface TashkentMetroEntry {
|
|
19
|
-
readonly canonical: string;
|
|
20
|
-
readonly name: string;
|
|
21
|
-
readonly type: 'metro';
|
|
22
|
-
readonly country: 'UZ';
|
|
23
|
-
readonly city: 'Tashkent';
|
|
24
|
-
readonly line: string;
|
|
25
|
-
readonly labels: Readonly<{ ru: string; en: string }>;
|
|
26
|
-
readonly aliases: readonly string[];
|
|
27
|
-
readonly re: RegExp;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export const TASHKENT_HOUSING_LANDMARKS: readonly TashkentHousingLocationEntry[];
|
|
31
|
-
export const TASHKENT_NUMBERED_AREA_ALIASES: Readonly<Record<string, readonly string[]>>;
|
|
32
|
-
|
|
33
|
-
export function matchTashkentHousingLandmarks(value: unknown): TashkentHousingLocationEntry[];
|
|
34
|
-
export function matchTashkentNumberedArea(value: unknown, canonical: string): TashkentNumberedAreaMatch | null;
|
|
35
|
-
export function hasTashkentAreaAlias(value: unknown, canonical: string): boolean;
|
|
36
|
-
export function hasExplicitTashkentDistrict(value: unknown, canonical: string): boolean;
|
|
37
|
-
export function matchTashkentHousingMetro(value: unknown): TashkentMetroEntry | null;
|
|
38
|
-
export function matchTashkentHousingTransit(value: unknown): TashkentHousingLocationEntry | null;
|
|
1
|
+
export interface TashkentHousingLocationEntry {
|
|
2
|
+
readonly canonical: string;
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly category: string;
|
|
5
|
+
readonly country: 'UZ';
|
|
6
|
+
readonly city: 'Tashkent';
|
|
7
|
+
readonly aliases: readonly string[];
|
|
8
|
+
readonly re: RegExp;
|
|
9
|
+
readonly contextRequired: boolean;
|
|
10
|
+
readonly contextRe: RegExp | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TashkentNumberedAreaMatch {
|
|
14
|
+
readonly number: number;
|
|
15
|
+
readonly suffix: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TashkentMetroEntry {
|
|
19
|
+
readonly canonical: string;
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly type: 'metro';
|
|
22
|
+
readonly country: 'UZ';
|
|
23
|
+
readonly city: 'Tashkent';
|
|
24
|
+
readonly line: string;
|
|
25
|
+
readonly labels: Readonly<{ ru: string; en: string }>;
|
|
26
|
+
readonly aliases: readonly string[];
|
|
27
|
+
readonly re: RegExp;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const TASHKENT_HOUSING_LANDMARKS: readonly TashkentHousingLocationEntry[];
|
|
31
|
+
export const TASHKENT_NUMBERED_AREA_ALIASES: Readonly<Record<string, readonly string[]>>;
|
|
32
|
+
|
|
33
|
+
export function matchTashkentHousingLandmarks(value: unknown): TashkentHousingLocationEntry[];
|
|
34
|
+
export function matchTashkentNumberedArea(value: unknown, canonical: string): TashkentNumberedAreaMatch | null;
|
|
35
|
+
export function hasTashkentAreaAlias(value: unknown, canonical: string): boolean;
|
|
36
|
+
export function hasExplicitTashkentDistrict(value: unknown, canonical: string): boolean;
|
|
37
|
+
export function matchTashkentHousingMetro(value: unknown): TashkentMetroEntry | null;
|
|
38
|
+
export function matchTashkentHousingTransit(value: unknown): TashkentHousingLocationEntry | null;
|
|
@@ -1,211 +1,211 @@
|
|
|
1
|
-
import { aliasesToRegex, escapeRegex, normalizeForMatch } from './normalization.js';
|
|
2
|
-
import { TASHKENT_METRO } from './geo.js';
|
|
3
|
-
|
|
4
|
-
function locationEntry(name, category, aliases = [], options = {}) {
|
|
5
|
-
const all = [...new Set([name, ...aliases].filter(Boolean))];
|
|
6
|
-
return Object.freeze({
|
|
7
|
-
canonical: name,
|
|
8
|
-
name,
|
|
9
|
-
category,
|
|
10
|
-
country: 'UZ',
|
|
11
|
-
city: 'Tashkent',
|
|
12
|
-
aliases: Object.freeze(all),
|
|
13
|
-
re: aliasesToRegex(all),
|
|
14
|
-
contextRequired: Boolean(options.contextRequired),
|
|
15
|
-
contextRe: options.context ? new RegExp(options.context, 'iu') : null,
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Housing-oriented Tashkent names that occur in listings as landmarks even when
|
|
21
|
-
* the generic POI catalogue cannot safely classify the bare token on its own.
|
|
22
|
-
* Canonicals are intentionally stable for existing parser consumers.
|
|
23
|
-
*/
|
|
24
|
-
export const TASHKENT_HOUSING_LANDMARKS = Object.freeze([
|
|
25
|
-
locationEntry('Alay Bazaar', 'market', [
|
|
26
|
-
'Алай', 'Алайский', 'Алайского', 'Alay', 'Oloy',
|
|
27
|
-
]),
|
|
28
|
-
locationEntry('C-2', 'local_area', ['Ц-2', 'Ц 2', 'C-2', 'C 2']),
|
|
29
|
-
locationEntry('Darkhan', 'local_area', ['Дархан', 'Darkhan', 'Darhan', 'Darxon']),
|
|
30
|
-
locationEntry('Novomoskovskaya', 'residential_complex', [
|
|
31
|
-
'Новомосковская', 'Новомосковской', 'Novomoskovskaya',
|
|
32
|
-
]),
|
|
33
|
-
locationEntry('Yangi Choshtepa', 'local_area', [
|
|
34
|
-
'Янги Чоштепа', 'Янги чоштепа', 'Yangi Choshtepa',
|
|
35
|
-
]),
|
|
36
|
-
locationEntry('Glinka', 'landmark', [
|
|
37
|
-
'Глинка', 'Glinka', 'Глинка ГАИ', 'ГАИ Глинка',
|
|
38
|
-
]),
|
|
39
|
-
locationEntry('Dehqonobod', 'landmark', [
|
|
40
|
-
'Дехконабад', 'Дехканабад', 'Дехконобод', 'Деҳқонобод', 'Dehqonobod', 'Dehkanabad',
|
|
41
|
-
]),
|
|
42
|
-
locationEntry('Sergeli Car Bazaar', 'market', [
|
|
43
|
-
'Сергели машинный базар',
|
|
44
|
-
'Сергели машина бозор',
|
|
45
|
-
'Сергели машина бозори',
|
|
46
|
-
'Sergeli mashina bozori',
|
|
47
|
-
'Sergeli mashina bozor',
|
|
48
|
-
'Sergeli moshina bozori',
|
|
49
|
-
'Sergeli moshina bozor',
|
|
50
|
-
'Sergile moshena bozor',
|
|
51
|
-
'Sergile moshina bozor',
|
|
52
|
-
'Sergele moshena bozor',
|
|
53
|
-
'Sergele moshina bozor',
|
|
54
|
-
]),
|
|
55
|
-
locationEntry('Nizami Pedagogical University', 'university', [
|
|
56
|
-
'Nizomiy',
|
|
57
|
-
'Nizomiy universiteti',
|
|
58
|
-
'Nizomiy pedagogika universiteti',
|
|
59
|
-
'Nizomiy nomidagi universitet',
|
|
60
|
-
'Низомий',
|
|
61
|
-
'Низомий университети',
|
|
62
|
-
'Низомий педагогика университети',
|
|
63
|
-
'Nizami Pedagogical University',
|
|
64
|
-
'National Pedagogical University of Uzbekistan named after Nizami',
|
|
65
|
-
'Tashkent State Pedagogical University named after Nizami',
|
|
66
|
-
'Ташкентский государственный педагогический университет имени Низами',
|
|
67
|
-
], { contextRequired: true, context: '(?:universitet|университет|pedagog|педагог)' }),
|
|
68
|
-
locationEntry('World Languages University', 'university', [
|
|
69
|
-
'Jahon tillar',
|
|
70
|
-
'Jahon tillar universiteti',
|
|
71
|
-
'Jahon tillari universiteti',
|
|
72
|
-
'Жахон тиллар',
|
|
73
|
-
'Жаҳон тиллар',
|
|
74
|
-
'Жахон тиллар университети',
|
|
75
|
-
'Жаҳон тиллар университети',
|
|
76
|
-
'Жаҳон тиллари университети',
|
|
77
|
-
'World Languages University',
|
|
78
|
-
'Uzbekistan State World Languages University',
|
|
79
|
-
'UzSWLU',
|
|
80
|
-
"O'zbekiston davlat jahon tillari universiteti",
|
|
81
|
-
'O‘zbekiston davlat jahon tillari universiteti',
|
|
82
|
-
'Oʻzbekiston davlat jahon tillari universiteti',
|
|
83
|
-
], { contextRequired: true, context: '(?:universitet|университет|university)' }),
|
|
84
|
-
]);
|
|
85
|
-
|
|
86
|
-
function hasRequiredContext(entry, text, start, end) {
|
|
87
|
-
if (!entry.contextRequired) return true;
|
|
88
|
-
const around = text.slice(Math.max(0, start - 48), Math.min(text.length, end + 48));
|
|
89
|
-
return entry.contextRe?.test(around) ?? false;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Return all housing landmarks in stable catalogue-priority order. */
|
|
93
|
-
export function matchTashkentHousingLandmarks(value) {
|
|
94
|
-
const text = String(value ?? '');
|
|
95
|
-
if (!text) return [];
|
|
96
|
-
const matches = [];
|
|
97
|
-
for (const entry of TASHKENT_HOUSING_LANDMARKS) {
|
|
98
|
-
const match = text.match(entry.re);
|
|
99
|
-
if (!match) continue;
|
|
100
|
-
const start = match.index ?? 0;
|
|
101
|
-
const end = start + match[0].length;
|
|
102
|
-
if (!hasRequiredContext(entry, text, start, end)) continue;
|
|
103
|
-
matches.push(entry);
|
|
104
|
-
}
|
|
105
|
-
return matches;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export const TASHKENT_NUMBERED_AREA_ALIASES = Object.freeze({
|
|
109
|
-
Chilanzar: Object.freeze(['чиланзар', 'чилонзор', 'chilanzar', 'chilonzor']),
|
|
110
|
-
Kuylyuk: Object.freeze(['куйлюк', 'куйлик', 'kuylyuk', 'kuyliq', 'qoyliq', 'qo yliq']),
|
|
111
|
-
Sergeli: Object.freeze(['сергели', 'sergeli', 'sergile', 'sergele']),
|
|
112
|
-
Yunusabad: Object.freeze(['юнусабад', 'yunusabad', 'yunusobod']),
|
|
113
|
-
Yangihayot: Object.freeze(['янгихаёт', 'янгихаят', 'yangihayot']),
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const REVERSE_NUMBERED_ALIASES = Object.freeze({
|
|
117
|
-
Chilanzar: Object.freeze(['чиланзара', 'чилонзора', 'chilanzar', 'chilonzor']),
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
const DISTRICT_CONTEXT_ALIASES = Object.freeze({
|
|
121
|
-
Sergeli: Object.freeze(['сергелийский', 'сергели', 'sergeli', 'sergile', 'sergele']),
|
|
122
|
-
Chilanzar: Object.freeze(['чиланзарский', 'чиланзар', 'чилонзор', 'chilanzar', 'chilonzor']),
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
const NUMBERED_CONTEXT = '(?:tumani|тумани|district|район|massiv|массив)';
|
|
126
|
-
const NUMBERED_SUFFIX_CONTEXT = '(?:chi|чи|й|квартал|kvartal|hudud(?:da)?|худуд(?:да)?)';
|
|
127
|
-
const DISTRICT_MARKER = '(?:район|туман\\p{L}*|tumani|district)';
|
|
128
|
-
|
|
129
|
-
function normalizedAlternatives(values) {
|
|
130
|
-
return values
|
|
131
|
-
.map((value) => escapeRegex(normalizeForMatch(value)).replace(/\\ /g, '\\s+'))
|
|
132
|
-
.filter(Boolean)
|
|
133
|
-
.join('|');
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Parse only the lexical shell of a numbered Tashkent massif. District-number
|
|
138
|
-
* ambiguity remains the responsibility of the consumer.
|
|
139
|
-
*/
|
|
140
|
-
export function matchTashkentNumberedArea(value, canonical) {
|
|
141
|
-
const aliases = TASHKENT_NUMBERED_AREA_ALIASES[canonical];
|
|
142
|
-
if (!aliases?.length) return null;
|
|
143
|
-
const text = normalizeForMatch(value);
|
|
144
|
-
if (!text) return null;
|
|
145
|
-
const alternatives = normalizedAlternatives(aliases);
|
|
146
|
-
let match = text.match(new RegExp(
|
|
147
|
-
`(?:^|\\s)(?:${alternatives})(?:\\s+${NUMBERED_CONTEXT})?\\s+(\\d{1,2})(?:\\s*([adад]))?(?:\\s+${NUMBERED_SUFFIX_CONTEXT})*(?:\\s|$)`,
|
|
148
|
-
'iu',
|
|
149
|
-
));
|
|
150
|
-
|
|
151
|
-
if (!match && REVERSE_NUMBERED_ALIASES[canonical]?.length) {
|
|
152
|
-
const reverse = normalizedAlternatives(REVERSE_NUMBERED_ALIASES[canonical]);
|
|
153
|
-
const reverseMatch = text.match(new RegExp(
|
|
154
|
-
`(?:^|\\s)(\\d{1,2})\\s+(?:квартал|кв\\s+л|kvartal)\\s+(?:${reverse})(?:\\s|$)`,
|
|
155
|
-
'iu',
|
|
156
|
-
));
|
|
157
|
-
if (reverseMatch) match = [reverseMatch[0], reverseMatch[1], ''];
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (!match) return null;
|
|
161
|
-
return Object.freeze({ number: Number(match[1]), suffix: match[2] || '' });
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export function hasTashkentAreaAlias(value, canonical) {
|
|
165
|
-
const aliases = TASHKENT_NUMBERED_AREA_ALIASES[canonical];
|
|
166
|
-
if (!aliases?.length) return false;
|
|
167
|
-
const text = normalizeForMatch(value);
|
|
168
|
-
return aliases.some((alias) => ` ${text} `.includes(` ${normalizeForMatch(alias)} `));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export function hasExplicitTashkentDistrict(value, canonical) {
|
|
172
|
-
const aliases = DISTRICT_CONTEXT_ALIASES[canonical];
|
|
173
|
-
if (!aliases?.length) return false;
|
|
174
|
-
const alternatives = normalizedAlternatives(aliases);
|
|
175
|
-
const text = normalizeForMatch(value);
|
|
176
|
-
return new RegExp(`(?:^|\\s)(?:${alternatives})\\s+${DISTRICT_MARKER}(?:\\s|$)`, 'iu').test(text);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const EXTRA_METRO_ALIASES = Object.freeze({
|
|
180
|
-
Sergeli: Object.freeze(['Sergile', 'Sergele']),
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
/** Resolve listing typos/transliterations to an existing canonical metro entry. */
|
|
184
|
-
export function matchTashkentHousingMetro(value) {
|
|
185
|
-
const text = String(value ?? '');
|
|
186
|
-
if (!text) return null;
|
|
187
|
-
for (const station of TASHKENT_METRO) {
|
|
188
|
-
if (station.re.test(text)) return station;
|
|
189
|
-
}
|
|
190
|
-
for (const [canonical, aliases] of Object.entries(EXTRA_METRO_ALIASES)) {
|
|
191
|
-
if (!aliasesToRegex(aliases).test(text)) continue;
|
|
192
|
-
return TASHKENT_METRO.find((station) => station.name === canonical) || null;
|
|
193
|
-
}
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const TASHKENT_HOUSING_TRANSIT = Object.freeze([
|
|
198
|
-
locationEntry('Tashkent North Railway Station', 'railway', [
|
|
199
|
-
'Ташкент Северный вокзал',
|
|
200
|
-
'Ташкент Северный',
|
|
201
|
-
'Tashkent North Railway Station',
|
|
202
|
-
'Toshkent North Railway Station',
|
|
203
|
-
]),
|
|
204
|
-
]);
|
|
205
|
-
|
|
206
|
-
/** Preserve the historical housing-parser label while keeping its aliases shared. */
|
|
207
|
-
export function matchTashkentHousingTransit(value) {
|
|
208
|
-
const text = String(value ?? '');
|
|
209
|
-
if (!text) return null;
|
|
210
|
-
return TASHKENT_HOUSING_TRANSIT.find((entry) => entry.re.test(text)) || null;
|
|
211
|
-
}
|
|
1
|
+
import { aliasesToRegex, escapeRegex, normalizeForMatch } from './normalization.js';
|
|
2
|
+
import { TASHKENT_METRO } from './geo.js';
|
|
3
|
+
|
|
4
|
+
function locationEntry(name, category, aliases = [], options = {}) {
|
|
5
|
+
const all = [...new Set([name, ...aliases].filter(Boolean))];
|
|
6
|
+
return Object.freeze({
|
|
7
|
+
canonical: name,
|
|
8
|
+
name,
|
|
9
|
+
category,
|
|
10
|
+
country: 'UZ',
|
|
11
|
+
city: 'Tashkent',
|
|
12
|
+
aliases: Object.freeze(all),
|
|
13
|
+
re: aliasesToRegex(all),
|
|
14
|
+
contextRequired: Boolean(options.contextRequired),
|
|
15
|
+
contextRe: options.context ? new RegExp(options.context, 'iu') : null,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Housing-oriented Tashkent names that occur in listings as landmarks even when
|
|
21
|
+
* the generic POI catalogue cannot safely classify the bare token on its own.
|
|
22
|
+
* Canonicals are intentionally stable for existing parser consumers.
|
|
23
|
+
*/
|
|
24
|
+
export const TASHKENT_HOUSING_LANDMARKS = Object.freeze([
|
|
25
|
+
locationEntry('Alay Bazaar', 'market', [
|
|
26
|
+
'Алай', 'Алайский', 'Алайского', 'Alay', 'Oloy',
|
|
27
|
+
]),
|
|
28
|
+
locationEntry('C-2', 'local_area', ['Ц-2', 'Ц 2', 'C-2', 'C 2']),
|
|
29
|
+
locationEntry('Darkhan', 'local_area', ['Дархан', 'Darkhan', 'Darhan', 'Darxon']),
|
|
30
|
+
locationEntry('Novomoskovskaya', 'residential_complex', [
|
|
31
|
+
'Новомосковская', 'Новомосковской', 'Novomoskovskaya',
|
|
32
|
+
]),
|
|
33
|
+
locationEntry('Yangi Choshtepa', 'local_area', [
|
|
34
|
+
'Янги Чоштепа', 'Янги чоштепа', 'Yangi Choshtepa',
|
|
35
|
+
]),
|
|
36
|
+
locationEntry('Glinka', 'landmark', [
|
|
37
|
+
'Глинка', 'Glinka', 'Глинка ГАИ', 'ГАИ Глинка',
|
|
38
|
+
]),
|
|
39
|
+
locationEntry('Dehqonobod', 'landmark', [
|
|
40
|
+
'Дехконабад', 'Дехканабад', 'Дехконобод', 'Деҳқонобод', 'Dehqonobod', 'Dehkanabad',
|
|
41
|
+
]),
|
|
42
|
+
locationEntry('Sergeli Car Bazaar', 'market', [
|
|
43
|
+
'Сергели машинный базар',
|
|
44
|
+
'Сергели машина бозор',
|
|
45
|
+
'Сергели машина бозори',
|
|
46
|
+
'Sergeli mashina bozori',
|
|
47
|
+
'Sergeli mashina bozor',
|
|
48
|
+
'Sergeli moshina bozori',
|
|
49
|
+
'Sergeli moshina bozor',
|
|
50
|
+
'Sergile moshena bozor',
|
|
51
|
+
'Sergile moshina bozor',
|
|
52
|
+
'Sergele moshena bozor',
|
|
53
|
+
'Sergele moshina bozor',
|
|
54
|
+
]),
|
|
55
|
+
locationEntry('Nizami Pedagogical University', 'university', [
|
|
56
|
+
'Nizomiy',
|
|
57
|
+
'Nizomiy universiteti',
|
|
58
|
+
'Nizomiy pedagogika universiteti',
|
|
59
|
+
'Nizomiy nomidagi universitet',
|
|
60
|
+
'Низомий',
|
|
61
|
+
'Низомий университети',
|
|
62
|
+
'Низомий педагогика университети',
|
|
63
|
+
'Nizami Pedagogical University',
|
|
64
|
+
'National Pedagogical University of Uzbekistan named after Nizami',
|
|
65
|
+
'Tashkent State Pedagogical University named after Nizami',
|
|
66
|
+
'Ташкентский государственный педагогический университет имени Низами',
|
|
67
|
+
], { contextRequired: true, context: '(?:universitet|университет|pedagog|педагог)' }),
|
|
68
|
+
locationEntry('World Languages University', 'university', [
|
|
69
|
+
'Jahon tillar',
|
|
70
|
+
'Jahon tillar universiteti',
|
|
71
|
+
'Jahon tillari universiteti',
|
|
72
|
+
'Жахон тиллар',
|
|
73
|
+
'Жаҳон тиллар',
|
|
74
|
+
'Жахон тиллар университети',
|
|
75
|
+
'Жаҳон тиллар университети',
|
|
76
|
+
'Жаҳон тиллари университети',
|
|
77
|
+
'World Languages University',
|
|
78
|
+
'Uzbekistan State World Languages University',
|
|
79
|
+
'UzSWLU',
|
|
80
|
+
"O'zbekiston davlat jahon tillari universiteti",
|
|
81
|
+
'O‘zbekiston davlat jahon tillari universiteti',
|
|
82
|
+
'Oʻzbekiston davlat jahon tillari universiteti',
|
|
83
|
+
], { contextRequired: true, context: '(?:universitet|университет|university)' }),
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
function hasRequiredContext(entry, text, start, end) {
|
|
87
|
+
if (!entry.contextRequired) return true;
|
|
88
|
+
const around = text.slice(Math.max(0, start - 48), Math.min(text.length, end + 48));
|
|
89
|
+
return entry.contextRe?.test(around) ?? false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Return all housing landmarks in stable catalogue-priority order. */
|
|
93
|
+
export function matchTashkentHousingLandmarks(value) {
|
|
94
|
+
const text = String(value ?? '');
|
|
95
|
+
if (!text) return [];
|
|
96
|
+
const matches = [];
|
|
97
|
+
for (const entry of TASHKENT_HOUSING_LANDMARKS) {
|
|
98
|
+
const match = text.match(entry.re);
|
|
99
|
+
if (!match) continue;
|
|
100
|
+
const start = match.index ?? 0;
|
|
101
|
+
const end = start + match[0].length;
|
|
102
|
+
if (!hasRequiredContext(entry, text, start, end)) continue;
|
|
103
|
+
matches.push(entry);
|
|
104
|
+
}
|
|
105
|
+
return matches;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const TASHKENT_NUMBERED_AREA_ALIASES = Object.freeze({
|
|
109
|
+
Chilanzar: Object.freeze(['чиланзар', 'чилонзор', 'chilanzar', 'chilonzor']),
|
|
110
|
+
Kuylyuk: Object.freeze(['куйлюк', 'куйлик', 'kuylyuk', 'kuyliq', 'qoyliq', 'qo yliq']),
|
|
111
|
+
Sergeli: Object.freeze(['сергели', 'sergeli', 'sergile', 'sergele']),
|
|
112
|
+
Yunusabad: Object.freeze(['юнусабад', 'yunusabad', 'yunusobod']),
|
|
113
|
+
Yangihayot: Object.freeze(['янгихаёт', 'янгихаят', 'yangihayot']),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const REVERSE_NUMBERED_ALIASES = Object.freeze({
|
|
117
|
+
Chilanzar: Object.freeze(['чиланзара', 'чилонзора', 'chilanzar', 'chilonzor']),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const DISTRICT_CONTEXT_ALIASES = Object.freeze({
|
|
121
|
+
Sergeli: Object.freeze(['сергелийский', 'сергели', 'sergeli', 'sergile', 'sergele']),
|
|
122
|
+
Chilanzar: Object.freeze(['чиланзарский', 'чиланзар', 'чилонзор', 'chilanzar', 'chilonzor']),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const NUMBERED_CONTEXT = '(?:tumani|тумани|district|район|massiv|массив)';
|
|
126
|
+
const NUMBERED_SUFFIX_CONTEXT = '(?:chi|чи|й|квартал|kvartal|hudud(?:da)?|худуд(?:да)?)';
|
|
127
|
+
const DISTRICT_MARKER = '(?:район|туман\\p{L}*|tumani|district)';
|
|
128
|
+
|
|
129
|
+
function normalizedAlternatives(values) {
|
|
130
|
+
return values
|
|
131
|
+
.map((value) => escapeRegex(normalizeForMatch(value)).replace(/\\ /g, '\\s+'))
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
.join('|');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Parse only the lexical shell of a numbered Tashkent massif. District-number
|
|
138
|
+
* ambiguity remains the responsibility of the consumer.
|
|
139
|
+
*/
|
|
140
|
+
export function matchTashkentNumberedArea(value, canonical) {
|
|
141
|
+
const aliases = TASHKENT_NUMBERED_AREA_ALIASES[canonical];
|
|
142
|
+
if (!aliases?.length) return null;
|
|
143
|
+
const text = normalizeForMatch(value);
|
|
144
|
+
if (!text) return null;
|
|
145
|
+
const alternatives = normalizedAlternatives(aliases);
|
|
146
|
+
let match = text.match(new RegExp(
|
|
147
|
+
`(?:^|\\s)(?:${alternatives})(?:\\s+${NUMBERED_CONTEXT})?\\s+(\\d{1,2})(?:\\s*([adад]))?(?:\\s+${NUMBERED_SUFFIX_CONTEXT})*(?:\\s|$)`,
|
|
148
|
+
'iu',
|
|
149
|
+
));
|
|
150
|
+
|
|
151
|
+
if (!match && REVERSE_NUMBERED_ALIASES[canonical]?.length) {
|
|
152
|
+
const reverse = normalizedAlternatives(REVERSE_NUMBERED_ALIASES[canonical]);
|
|
153
|
+
const reverseMatch = text.match(new RegExp(
|
|
154
|
+
`(?:^|\\s)(\\d{1,2})\\s+(?:квартал|кв\\s+л|kvartal)\\s+(?:${reverse})(?:\\s|$)`,
|
|
155
|
+
'iu',
|
|
156
|
+
));
|
|
157
|
+
if (reverseMatch) match = [reverseMatch[0], reverseMatch[1], ''];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!match) return null;
|
|
161
|
+
return Object.freeze({ number: Number(match[1]), suffix: match[2] || '' });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function hasTashkentAreaAlias(value, canonical) {
|
|
165
|
+
const aliases = TASHKENT_NUMBERED_AREA_ALIASES[canonical];
|
|
166
|
+
if (!aliases?.length) return false;
|
|
167
|
+
const text = normalizeForMatch(value);
|
|
168
|
+
return aliases.some((alias) => ` ${text} `.includes(` ${normalizeForMatch(alias)} `));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function hasExplicitTashkentDistrict(value, canonical) {
|
|
172
|
+
const aliases = DISTRICT_CONTEXT_ALIASES[canonical];
|
|
173
|
+
if (!aliases?.length) return false;
|
|
174
|
+
const alternatives = normalizedAlternatives(aliases);
|
|
175
|
+
const text = normalizeForMatch(value);
|
|
176
|
+
return new RegExp(`(?:^|\\s)(?:${alternatives})\\s+${DISTRICT_MARKER}(?:\\s|$)`, 'iu').test(text);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const EXTRA_METRO_ALIASES = Object.freeze({
|
|
180
|
+
Sergeli: Object.freeze(['Sergile', 'Sergele']),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
/** Resolve listing typos/transliterations to an existing canonical metro entry. */
|
|
184
|
+
export function matchTashkentHousingMetro(value) {
|
|
185
|
+
const text = String(value ?? '');
|
|
186
|
+
if (!text) return null;
|
|
187
|
+
for (const station of TASHKENT_METRO) {
|
|
188
|
+
if (station.re.test(text)) return station;
|
|
189
|
+
}
|
|
190
|
+
for (const [canonical, aliases] of Object.entries(EXTRA_METRO_ALIASES)) {
|
|
191
|
+
if (!aliasesToRegex(aliases).test(text)) continue;
|
|
192
|
+
return TASHKENT_METRO.find((station) => station.name === canonical) || null;
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const TASHKENT_HOUSING_TRANSIT = Object.freeze([
|
|
198
|
+
locationEntry('Tashkent North Railway Station', 'railway', [
|
|
199
|
+
'Ташкент Северный вокзал',
|
|
200
|
+
'Ташкент Северный',
|
|
201
|
+
'Tashkent North Railway Station',
|
|
202
|
+
'Toshkent North Railway Station',
|
|
203
|
+
]),
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
/** Preserve the historical housing-parser label while keeping its aliases shared. */
|
|
207
|
+
export function matchTashkentHousingTransit(value) {
|
|
208
|
+
const text = String(value ?? '');
|
|
209
|
+
if (!text) return null;
|
|
210
|
+
return TASHKENT_HOUSING_TRANSIT.find((entry) => entry.re.test(text)) || null;
|
|
211
|
+
}
|