@whiteslove/parsing-lexicon 0.2.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/LICENSE +21 -0
- package/README.md +137 -0
- package/index.d.ts +375 -0
- package/package.json +75 -0
- package/src/central-asia-locations.js +211 -0
- package/src/central-asia.js +233 -0
- package/src/contact.d.ts +44 -0
- package/src/contact.js +159 -0
- package/src/countries.js +175 -0
- package/src/country-context.d.ts +10 -0
- package/src/country-context.js +22 -0
- package/src/currency.d.ts +14 -0
- package/src/currency.js +66 -0
- package/src/geo.js +169 -0
- package/src/geography-central-asia.js +42 -0
- package/src/geography-detection.d.ts +14 -0
- package/src/geography-detection.js +64 -0
- package/src/geography-display.d.ts +13 -0
- package/src/geography-display.js +113 -0
- package/src/geography.js +210 -0
- package/src/hiring-advanced.js +147 -0
- package/src/hiring-candidate-fields.d.ts +9 -0
- package/src/hiring-candidate-fields.js +92 -0
- package/src/hiring-context.d.ts +64 -0
- package/src/hiring-context.js +412 -0
- package/src/hiring-language-extensions.d.ts +19 -0
- package/src/hiring-language-extensions.js +139 -0
- package/src/hiring-languages.js +135 -0
- package/src/hiring-location-fields.d.ts +3 -0
- package/src/hiring-location-fields.js +53 -0
- package/src/hiring-professions-ro.js +191 -0
- package/src/hiring-professions.js +437 -0
- package/src/hiring-requirements.d.ts +16 -0
- package/src/hiring-requirements.js +170 -0
- package/src/hiring-salary-context.d.ts +27 -0
- package/src/hiring-salary-context.js +76 -0
- package/src/hiring-semantics.d.ts +29 -0
- package/src/hiring-semantics.js +218 -0
- package/src/hiring-skills.d.ts +21 -0
- package/src/hiring-skills.js +315 -0
- package/src/hiring-source-aliases.d.ts +32 -0
- package/src/hiring-source-aliases.js +186 -0
- package/src/hiring-source-semantics.d.ts +31 -0
- package/src/hiring-source-semantics.js +293 -0
- package/src/hiring-temporal.d.ts +15 -0
- package/src/hiring-temporal.js +67 -0
- package/src/hiring-vacancy-fields.d.ts +5 -0
- package/src/hiring-vacancy-fields.js +23 -0
- package/src/hiring-work-semantics.d.ts +34 -0
- package/src/hiring-work-semantics.js +72 -0
- package/src/hiring.js +180 -0
- package/src/housing-address.d.ts +18 -0
- package/src/housing-address.js +210 -0
- package/src/housing-context.d.ts +36 -0
- package/src/housing-context.js +183 -0
- package/src/housing-features.js +30 -0
- package/src/housing-intent.d.ts +9 -0
- package/src/housing-intent.js +132 -0
- package/src/housing-listing-fields.js +131 -0
- package/src/housing-money.d.ts +7 -0
- package/src/housing-money.js +102 -0
- package/src/housing-source-aliases.d.ts +8 -0
- package/src/housing-source-aliases.js +51 -0
- package/src/housing-structured.d.ts +55 -0
- package/src/housing-structured.js +219 -0
- package/src/housing.js +200 -0
- package/src/index.js +44 -0
- package/src/kz-location-extensions.js +307 -0
- package/src/landmarks.js +21 -0
- package/src/lexicon-core.js +58 -0
- package/src/location-merge.js +106 -0
- package/src/locations.js +456 -0
- package/src/money-core.d.ts +10 -0
- package/src/money-core.js +81 -0
- package/src/money-lexicon.d.ts +5 -0
- package/src/money-lexicon.js +79 -0
- package/src/money.js +154 -0
- package/src/normalization.js +384 -0
- package/src/odesa-metropolitan.js +145 -0
- package/src/romania-geography.js +47 -0
- package/src/tashkent-colloquial.js +72 -0
- package/src/tashkent-housing-geography.d.ts +38 -0
- package/src/tashkent-housing-geography.js +211 -0
- package/src/tashkent-pois.js +215 -0
- package/src/tashkent-residential-complexes.js +272 -0
- package/src/ua-location-extensions-major.js +118 -0
- package/src/ua-location-extensions-metro.js +9 -0
- package/src/ua-location-extensions-regional.js +246 -0
- package/src/ua-secondary-cities.d.ts +2 -0
- package/src/ua-secondary-cities.js +57 -0
- package/src/ukraine.js +75 -0
- package/src/uz-location-extensions.js +259 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { aliasesToRegex } from './normalization.js';
|
|
2
|
+
|
|
3
|
+
function entity(name, type, aliases = [], options = {}) {
|
|
4
|
+
const all = [...new Set([name, ...aliases].filter(Boolean))];
|
|
5
|
+
return Object.freeze({
|
|
6
|
+
canonical: name,
|
|
7
|
+
name,
|
|
8
|
+
type,
|
|
9
|
+
aliases: Object.freeze(all),
|
|
10
|
+
re: aliasesToRegex(all),
|
|
11
|
+
parent: options.parent || null,
|
|
12
|
+
cluster: options.cluster || null,
|
|
13
|
+
contextRequired: Boolean(options.contextRequired),
|
|
14
|
+
contextRe: options.context ? new RegExp(options.context, 'iu') : null,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const NORTH_EAST_CLUSTER = 'Одеса — північно-східна агломерація';
|
|
19
|
+
|
|
20
|
+
export const ODESA_LOCAL_AREAS = Object.freeze([
|
|
21
|
+
entity('Житловий масив Котовського', 'local_area', [
|
|
22
|
+
'Посёлок Котовского', 'Поселок Котовского', 'селище Котовського', 'Котовского', 'Котовського',
|
|
23
|
+
'Котовка', 'Поскот', 'ПосКот', 'пос. Котовского', 'пос Котовского', 'п. Котовского', 'п.Котовского',
|
|
24
|
+
'ж/м Котовского', 'жилмассив Котовского', 'житловий масив Котовського', 'Kotovskoho', 'Kotovskogo',
|
|
25
|
+
'Kotovsky settlement', 'Старый Посёлок', 'Старий селище Котовського', 'Старый Котовского',
|
|
26
|
+
'Новый Котовского', 'Новий Котовського', 'Поле Чудес', 'Поле чудес', 'Сахарный', 'Цукровий',
|
|
27
|
+
], { cluster: NORTH_EAST_CLUSTER }),
|
|
28
|
+
entity('Малий Фонтан', 'local_area', [
|
|
29
|
+
'Малый Фонтан', 'Малий Фонтан', 'Малого Фонтана', 'Малого Фонтану',
|
|
30
|
+
'на Малом Фонтане', 'на Малому Фонтані', 'район Малого Фонтана', 'район Малого Фонтану',
|
|
31
|
+
'Malyi Fontan', 'Maly Fontan',
|
|
32
|
+
]),
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export const ODESA_MICRODISTRICT_EXTENSIONS = Object.freeze([
|
|
36
|
+
entity('Лузанівка', 'microdistrict', ['Лузановка','Luzanivka','Luzanovka','Лузановский район','Лузанівський район','район Лузановки','район Лузанівки','Лузановка пляж','Лузанівка пляж'], { cluster: NORTH_EAST_CLUSTER }),
|
|
37
|
+
entity('Вузівський', 'microdistrict', ['Вузовский','Вузівський','Вузовський','Vuzovskyi','Вузовский район','Вузівський район']),
|
|
38
|
+
entity('Чубаївка', 'microdistrict', ['Чубаевка','Чубаївка','Chubaivka','Chubayevka']),
|
|
39
|
+
entity('Дача Ковалевського', 'microdistrict', ['Дача Ковалевского','Дача Ковалевського','Дача Ковальского','Kovalevskoho Dacha','Kovalevskogo Dacha']),
|
|
40
|
+
entity('Золотий Берег', 'poi.beach', ['Золотой Берег','Золотий Берег','Golden Coast','16 станция Фонтана','16-та станція Великого Фонтану']),
|
|
41
|
+
entity('Чорноморка', 'microdistrict', ['Черноморка','Чорноморка','Chornomorka','Chernomorka','Люстдорф','Lustdorf']),
|
|
42
|
+
entity('Таїрова', 'microdistrict', ['Таирова','Таїрова','Tairova','район Таирова','ж/м Таирова','жилмассив Таирова']),
|
|
43
|
+
entity('Черемушки', 'microdistrict', ['Черёмушки','Черемушки','Cheremushky','Cheryomushki','Черёмушки Одесса','Черемушки Одеса']),
|
|
44
|
+
entity('Ближні Млини', 'microdistrict', ['Ближние Мельницы','Ближні Млини','Ближние Мельницы Одесса','Ближні Млини Одеса']),
|
|
45
|
+
entity('Дальні Млини', 'microdistrict', ['Дальние Мельницы','Дальні Млини','Дальние Мельницы Одесса','Дальні Млини Одеса']),
|
|
46
|
+
entity('Бугаївка', 'microdistrict', ['Бугаёвка','Бугаевка','Бугаївка','Buhaivka','Bugaevka']),
|
|
47
|
+
entity('Ленселище', 'microdistrict', ['Ленпосёлок','Ленпоселок','Ленселище','Ленпос','Ленпосёлок Одесса']),
|
|
48
|
+
entity('Застава', 'microdistrict', ['Застава','Застава-1','Застава-2','Застава I','Застава II','Первая Застава','Перша Застава','Вторая Застава','Друга Застава']),
|
|
49
|
+
entity('Слобідка', 'microdistrict', ['Слободка','Слобідка','Slobidka','Слободка Одесса','Слобідка Одеса']),
|
|
50
|
+
entity('Молдаванка', 'microdistrict', ['Молдаванка','Moldavanka','Молдаванка Одесса','Молдаванка Одеса']),
|
|
51
|
+
entity('Пересип', 'microdistrict', ['Пересыпь','Пересип','Peresyp','Пересип Одеса','Пересыпь Одесса']),
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
export const ODESA_SUBURBS = Object.freeze([
|
|
55
|
+
entity('Крижанівка', 'suburb', ['Крыжановка','Kryzhanivka','Kryzhanovka','с. Крыжановка','с. Крижанівка','село Крыжановка','село Крижанівка','Крыжановка Одесса','Крижанівка Одеса','район Крыжановки','район Крижанівки','Крыжановка / Котовского','Крижанівка / Котовського'], { cluster: NORTH_EAST_CLUSTER, parent: 'Фонтанська громада' }),
|
|
56
|
+
entity('Ліски', 'suburb', ['Лески','Liski','Lisky','с. Лески','селище Ліски','поселок Лески','Лески Одесса','Ліски Одеса','Лески / Крыжановка','Ліски / Крижанівка','Лески / Фонтанка','Ліски / Фонтанка'], { cluster: NORTH_EAST_CLUSTER, parent: 'Фонтанська громада' }),
|
|
57
|
+
entity('Фонтанка', 'suburb', ['Fontanka','с. Фонтанка','село Фонтанка','Фонтанка Одесса','Фонтанка Одеса','Одесса Фонтанка','Одеса Фонтанка','район Фонтанки','район Фонтанка','Fontanka Odesa','Fontanka Odessa'], { cluster: NORTH_EAST_CLUSTER, parent: 'Фонтанська громада' }),
|
|
58
|
+
entity('Вапнярка', 'suburb', ['Вапнярка Одеса','Вапнярка Одесса','Vapniarka','Vapnyarka','село Вапнярка'], { cluster: NORTH_EAST_CLUSTER, parent: 'Фонтанська громада' }),
|
|
59
|
+
entity('Нова Дофінівка', 'suburb', ['Новая Дофиновка','Нова Дофиновка','Nova Dofinivka','Novaya Dofinovka','Дофиновка','Дофінівка'], { cluster: NORTH_EAST_CLUSTER, parent: 'Фонтанська громада' }),
|
|
60
|
+
entity('Олександрівка', 'suburb', ['Александровка','Oleksandrivka','Aleksandrovka','Александровка Одесса','Олександрівка Одеський район'], { parent: 'Фонтанська громада' }),
|
|
61
|
+
entity('Світле', 'suburb', ['Светлое','Svitłe','Svitle','Светлое Одесса'], { parent: 'Фонтанська громада' }),
|
|
62
|
+
entity('Авангард', 'suburb', ['Avanhard','Avangard','Авангард Одесса','Авангард Одеса','ж/м 7 Небо','7 Небо','Седьмое небо']),
|
|
63
|
+
entity('Лиманка', 'suburb', ['Lymanka','Limanka','Лиманка Одесса','Лиманка Одеса','ж/м Радужный','Радужный массив','Райдужний масив','Мизикевича','Мізікевича','Mizikevycha','ж/м Мизикевича']),
|
|
64
|
+
entity('Червоний Хутір', 'suburb', ['Червоный Хутор','Красный Хутор','Chervonyi Khutir']),
|
|
65
|
+
entity('Таїрове', 'suburb', ['Таирово','Tairove','Tairovo','с. Таирово','смт Таїрове']),
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
export const ODESA_DEVELOPMENT_AREAS = Object.freeze([
|
|
69
|
+
entity('Совіньйон', 'development_area', ['Совиньон','Совіньйон','Sauvignon','Совиньон-1','Совиньон-2','Совиньон-3','Совиньон Марин Виллас']),
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
const MALL_CONTEXT = '(?:трц|тц|торгов(?:ый|ельний)|shopping|mall|возле|біля|рядом|напротив|навпроти|у\\s+ривьер|у\\s+рів.{0,2}єр)';
|
|
73
|
+
|
|
74
|
+
export const ODESA_RIVIERA_ENTITIES = Object.freeze([
|
|
75
|
+
entity('ТРЦ Рів’єра', 'poi.shopping_mall', ['Ривьера','Ривьеры','Рів’єра','Рів’єри','Riviera','ТРЦ Ривьера','ТРЦ Рів’єра','Riviera Shopping City','Riviera Mall','ТЦ Ривьера','ТЦ Рів’єра'], { cluster: NORTH_EAST_CLUSTER, parent: 'Фонтанка', contextRequired: true, context: MALL_CONTEXT }),
|
|
76
|
+
entity('Район ТРЦ Рів’єра', 'informal_area', ['район Ривьеры','район Рів’єри','у Ривьеры','возле Ривьеры','біля Рів’єри','рядом с Ривьерой','напротив Ривьеры','навпроти Рів’єри','Ривьера район','Riviera area'], { cluster: NORTH_EAST_CLUSTER, parent: 'Фонтанка' }),
|
|
77
|
+
entity('Сади Рів’єри', 'residential_complex', ['Сады Ривьеры','Сади Рів’єри','Сады Ривьера','Сади Рів’єра','Riviera Gardens','Gardens of Riviera','Ривьера Сити','Рів’єра Сіті','Riviera City'], { parent: 'Фонтанка' }),
|
|
78
|
+
entity('Чорноморська Рів’єра', 'development_area', ['Черноморская Ривьера','Чорноморська Рів\'єра','Black Sea Riviera','район Черноморской Ривьеры'], { parent: 'Фонтанка' }),
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
export const ODESA_CONTEXT_POIS = Object.freeze([
|
|
82
|
+
entity('Лузанівський пляж', 'poi.beach', ['пляж Лузановка','пляж Лузанівка'], { parent: 'Лузанівка', cluster: NORTH_EAST_CLUSTER }),
|
|
83
|
+
entity('Лузанівський парк', 'poi.park', ['Лузановский парк','Лузанівський парк','Лузановский лес','Лузанівський ліс'], { parent: 'Лузанівка', cluster: NORTH_EAST_CLUSTER }),
|
|
84
|
+
entity('Молода Гвардія', 'poi.landmark', ['Молодая Гвардия','Молода Гвардія'], { parent: 'Лузанівка', cluster: NORTH_EAST_CLUSTER }),
|
|
85
|
+
entity('411-та батарея', 'poi.memorial', ['Мемориал 411 батареи','411 батарея Одесса']),
|
|
86
|
+
entity('Південний ринок', 'poi.market', ['Южный рынок','Південний ринок','район Южного рынка','район Південного ринку']),
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
export const ODESA_SEARCH_CLUSTERS = Object.freeze([
|
|
90
|
+
Object.freeze({
|
|
91
|
+
canonical: NORTH_EAST_CLUSTER,
|
|
92
|
+
name: NORTH_EAST_CLUSTER,
|
|
93
|
+
type: 'search_cluster',
|
|
94
|
+
administrative: false,
|
|
95
|
+
members: Object.freeze(['Житловий масив Котовського','Лузанівка','Крижанівка','Ліски','Фонтанка','ТРЦ Рів’єра','Вапнярка','Нова Дофінівка']),
|
|
96
|
+
}),
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
export const ODESA_METROPOLITAN_ENTITIES = Object.freeze([
|
|
100
|
+
...ODESA_LOCAL_AREAS,
|
|
101
|
+
...ODESA_MICRODISTRICT_EXTENSIONS,
|
|
102
|
+
...ODESA_SUBURBS,
|
|
103
|
+
...ODESA_DEVELOPMENT_AREAS,
|
|
104
|
+
...ODESA_RIVIERA_ENTITIES,
|
|
105
|
+
...ODESA_CONTEXT_POIS,
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
function contextMatches(text, match, entity) {
|
|
109
|
+
if (!entity.contextRequired || !entity.contextRe) return true;
|
|
110
|
+
const start = match.index ?? 0;
|
|
111
|
+
const end = start + match[0].length;
|
|
112
|
+
const window = text.slice(Math.max(0, start - 48), Math.min(text.length, end + 48));
|
|
113
|
+
return entity.contextRe.test(window);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function matchOdesaMetropolitanEntities(value) {
|
|
117
|
+
const text = String(value ?? '');
|
|
118
|
+
if (!text) return Object.freeze({ matches: Object.freeze([]), searchClusters: Object.freeze([]) });
|
|
119
|
+
|
|
120
|
+
const found = [];
|
|
121
|
+
for (const item of ODESA_METROPOLITAN_ENTITIES) {
|
|
122
|
+
const match = text.match(item.re);
|
|
123
|
+
if (!match || !contextMatches(text, match, item)) continue;
|
|
124
|
+
found.push({ item, start: match.index ?? 0, length: match[0].length });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
found.sort((a, b) => a.start - b.start || b.length - a.length);
|
|
128
|
+
const unique = [];
|
|
129
|
+
const seen = new Set();
|
|
130
|
+
for (const { item } of found) {
|
|
131
|
+
const key = `${item.type}:${item.name}`;
|
|
132
|
+
if (seen.has(key)) continue;
|
|
133
|
+
seen.add(key);
|
|
134
|
+
unique.push(item);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const clusterNames = [...new Set(unique.map((item) => item.cluster).filter(Boolean))];
|
|
138
|
+
const searchClusters = ODESA_SEARCH_CLUSTERS.filter((cluster) => clusterNames.includes(cluster.name));
|
|
139
|
+
return Object.freeze({ matches: Object.freeze(unique), searchClusters: Object.freeze(searchClusters) });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function matchOdesaMetropolitanEntity(value, type = null) {
|
|
143
|
+
const result = matchOdesaMetropolitanEntities(value);
|
|
144
|
+
return result.matches.find((item) => !type || item.type === type) || null;
|
|
145
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { lexiconEntity } from './lexicon-core.js';
|
|
2
|
+
|
|
3
|
+
const city = (canonical, ro, aliases = []) => lexiconEntity(canonical, {
|
|
4
|
+
ro: [...new Set([ro, ...aliases])],
|
|
5
|
+
en: [canonical],
|
|
6
|
+
}, { country: 'RO', type: 'city' });
|
|
7
|
+
|
|
8
|
+
/** Romanian county seats and high-signal Bucharest metropolitan localities not covered by the core city seed. */
|
|
9
|
+
export const RO_CITY_EXTENSIONS = Object.freeze([
|
|
10
|
+
city('Alba Iulia', 'Alba Iulia'),
|
|
11
|
+
city('Pitesti', 'Pitești', ['Pitesti']),
|
|
12
|
+
city('Bacau', 'Bacău', ['Bacau']),
|
|
13
|
+
city('Bistrita', 'Bistrița', ['Bistrita']),
|
|
14
|
+
city('Botosani', 'Botoșani', ['Botosani']),
|
|
15
|
+
city('Braila', 'Brăila', ['Braila']),
|
|
16
|
+
city('Buzau', 'Buzău', ['Buzau']),
|
|
17
|
+
city('Resita', 'Reșița', ['Resita']),
|
|
18
|
+
city('Calarasi', 'Călărași', ['Calarasi']),
|
|
19
|
+
city('Sfantu Gheorghe', 'Sfântu Gheorghe', ['Sfantu Gheorghe']),
|
|
20
|
+
city('Targoviste', 'Târgoviște', ['Targoviste']),
|
|
21
|
+
city('Giurgiu', 'Giurgiu'),
|
|
22
|
+
city('Targu Jiu', 'Târgu Jiu', ['Targu Jiu']),
|
|
23
|
+
city('Miercurea Ciuc', 'Miercurea Ciuc'),
|
|
24
|
+
city('Deva', 'Deva'),
|
|
25
|
+
city('Slobozia', 'Slobozia'),
|
|
26
|
+
city('Baia Mare', 'Baia Mare'),
|
|
27
|
+
city('Drobeta-Turnu Severin', 'Drobeta-Turnu Severin', ['Drobeta Turnu Severin', 'Turnu Severin']),
|
|
28
|
+
city('Targu Mures', 'Târgu Mureș', ['Targu Mures']),
|
|
29
|
+
city('Piatra Neamt', 'Piatra Neamț', ['Piatra Neamt']),
|
|
30
|
+
city('Slatina', 'Slatina'),
|
|
31
|
+
city('Satu Mare', 'Satu Mare'),
|
|
32
|
+
city('Zalau', 'Zalău', ['Zalau']),
|
|
33
|
+
city('Suceava', 'Suceava'),
|
|
34
|
+
city('Alexandria', 'Alexandria'),
|
|
35
|
+
city('Tulcea', 'Tulcea'),
|
|
36
|
+
city('Vaslui', 'Vaslui'),
|
|
37
|
+
city('Ramnicu Valcea', 'Râmnicu Vâlcea', ['Ramnicu Valcea']),
|
|
38
|
+
city('Focsani', 'Focșani', ['Focsani']),
|
|
39
|
+
city('Buftea', 'Buftea'),
|
|
40
|
+
city('Otopeni', 'Otopeni'),
|
|
41
|
+
city('Voluntari', 'Voluntari'),
|
|
42
|
+
city('Popesti-Leordeni', 'Popești-Leordeni', ['Popesti-Leordeni', 'Popesti Leordeni']),
|
|
43
|
+
city('Bragadiru', 'Bragadiru'),
|
|
44
|
+
city('Pantelimon', 'Pantelimon'),
|
|
45
|
+
city('Chitila', 'Chitila'),
|
|
46
|
+
city('Magurele', 'Măgurele', ['Magurele']),
|
|
47
|
+
]);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { TASHKENT_AREAS } from './geo.js';
|
|
2
|
+
|
|
3
|
+
const area = (name, aliases) => Object.freeze({ name, aliases: Object.freeze(aliases) });
|
|
4
|
+
|
|
5
|
+
/** Additions to the core Tashkent catalog; entries here do not duplicate geo.js rows. */
|
|
6
|
+
export const TASHKENT_AREA_ADDITIONS = Object.freeze({
|
|
7
|
+
Almazar: Object.freeze([
|
|
8
|
+
area('Shifokorlar-1', ['шифокорлар 1', 'shifokorlar 1']),
|
|
9
|
+
area('Shifokorlar-2', ['шифокорлар 2', 'shifokorlar 2']),
|
|
10
|
+
area('Beruni-3', ['беруни 3', 'beruniy 3', 'beruni 3']),
|
|
11
|
+
area('Hislat', ['хислат', 'hislat', 'xislat']),
|
|
12
|
+
...[1, 2, 3, 4].map((n) => area(`Beshkurgan-${n}`, [`бешкурган ${n}`, `бешқўрғон ${n}`, `beshqo rg on ${n}`, `beshkurgan ${n}`])),
|
|
13
|
+
]),
|
|
14
|
+
Bektemir: Object.freeze([
|
|
15
|
+
area('Suvsoz-1', ['сувсоз 1', 'водник 1', 'suvsoz 1']),
|
|
16
|
+
area('Suvsoz-2', ['сувсоз 2', 'водник 2', 'suvsoz 2']),
|
|
17
|
+
area('Binokor', ['бинокор', 'binokor']),
|
|
18
|
+
area('Binokor-2', ['бинокор 2', 'binokor 2']),
|
|
19
|
+
area('Majnuntol', ['мажнунтол', 'majnuntol']),
|
|
20
|
+
area('Olima Oshirova', ['олима оширова', 'olima oshirova']),
|
|
21
|
+
area('Bektemir', ['бектемир массив', 'bektemir massivi']),
|
|
22
|
+
]),
|
|
23
|
+
Mirobod: Object.freeze([
|
|
24
|
+
area('Farovon', ['фаровон', 'farovon']),
|
|
25
|
+
area('Abdurauf Fitrat', ['абдурауф фитрат', 'abdurauf fitrat']),
|
|
26
|
+
]),
|
|
27
|
+
'Mirzo Ulugbek': Object.freeze([
|
|
28
|
+
area('Feruza-1', ['феруза 1', 'feruza 1']),
|
|
29
|
+
area('Buz-1', ['буз 1', 'бўз 1', 'boz 1', 'bo z 1']),
|
|
30
|
+
area('Turon', ['турон', 'turon']),
|
|
31
|
+
area('Riyoziy', ['риёзий', 'riyoziy']),
|
|
32
|
+
]),
|
|
33
|
+
Sergeli: Object.freeze([
|
|
34
|
+
area('Sergeli-2 G-40', ['сергели 2 г 40', 'sergeli 2 g 40']),
|
|
35
|
+
area('Babur Quarter', ['квартал бабур', 'бабур квартал', 'babur kvartal']),
|
|
36
|
+
]),
|
|
37
|
+
Uchtepa: Object.freeze([
|
|
38
|
+
area('Al-Khorezmi-2', ['аль хорезми 2', 'ал хорезми 2', 'al xorazmiy 2', 'al khorezmi 2']),
|
|
39
|
+
area('Shark', ['массив шарк', 'шарк массив', 'sharq massivi']),
|
|
40
|
+
]),
|
|
41
|
+
Chilanzar: Object.freeze([
|
|
42
|
+
area('Almazar Massif', ['массив алмазар', 'массив олмазор', 'almazar massivi', 'olmazor massivi']),
|
|
43
|
+
]),
|
|
44
|
+
Shaykhantahur: Object.freeze([
|
|
45
|
+
area('Gulabad', ['гульабад', 'гулабад', 'gulobod', 'gulabad', 'ц 26', 'c 26']),
|
|
46
|
+
area('Aktepa', ['актепа шайхантахур', 'oqtepa shayxontohur']),
|
|
47
|
+
area('Ibn Sino-1', ['ибн сино 1', 'ibn sino 1']),
|
|
48
|
+
area('Ibn Sino-2', ['ибн сино 2', 'ibn sino 2']),
|
|
49
|
+
area('Jarariq', ['джарарык', 'жарарик', 'jarariq', 'jararik']),
|
|
50
|
+
]),
|
|
51
|
+
Yunusabad: Object.freeze([
|
|
52
|
+
area('Katta Hasanboy', ['катта хасанбой', 'katta hasanboy']),
|
|
53
|
+
]),
|
|
54
|
+
Yakkasaray: Object.freeze([
|
|
55
|
+
area('Bobur', ['массив бобур', 'bobur massivi']),
|
|
56
|
+
area('Konstitutsiya', ['конституция массив', 'konstitutsiya massivi']),
|
|
57
|
+
area('Hamid Sulaymonov', ['хамид сулаймонов', 'hamid sulaymonov']),
|
|
58
|
+
]),
|
|
59
|
+
Yashnobod: Object.freeze([
|
|
60
|
+
area('Asalabad-1', ['асалабад 1', 'asalobod 1', 'asalabad 1']),
|
|
61
|
+
area('Asalabad-2', ['асалабад 2', 'asalobod 2', 'asalabad 2']),
|
|
62
|
+
area('Mumtoz', ['мумтаз', 'mumtoz']),
|
|
63
|
+
]),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export const FULL_TASHKENT_AREAS = Object.freeze(Object.fromEntries(
|
|
67
|
+
[...new Set([...Object.keys(TASHKENT_AREAS), ...Object.keys(TASHKENT_AREA_ADDITIONS)])]
|
|
68
|
+
.map((district) => [district, Object.freeze([
|
|
69
|
+
...(TASHKENT_AREAS[district] || []),
|
|
70
|
+
...(TASHKENT_AREA_ADDITIONS[district] || []),
|
|
71
|
+
])]),
|
|
72
|
+
));
|
|
@@ -0,0 +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;
|
|
@@ -0,0 +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
|
+
}
|