@whiteslove/parsing-lexicon 0.9.1 → 0.9.4
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 +1 -1
- package/src/central-asia-locations.js +16 -4
- package/src/housing-commercial.js +1 -1
- package/src/housing-intent.js +26 -1
- 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,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
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// residential housing feeds. Kept conservative so nearby amenities (for
|
|
3
3
|
// example "магазин рядом") do not classify an otherwise valid home as commercial.
|
|
4
4
|
const COMMERCIAL_HOUSING_RE =
|
|
5
|
-
/(офис[ _|,/.]|под ?офис|офисн|\boffice\b|\bofis\b|кеңсе|коммерческ|commercial|бизнес[ -]?центр|не\s?жил(?:ое|ой|ый|ых|ым|ого)|помещени[ея]|торгов(?:ое|ая) ?площад|торгов(?:ое|ое помещ)|warehouse
|
|
5
|
+
/(офис[ _|,/.]|под ?офис|офисн|\boffice\b|\bofis\b|кеңсе|коммерческ|commercial|бизнес[ -]?центр|не\s?жил(?:ое|ой|ый|ых|ым|ого)|помещени[ея]|торгов(?:ое|ая) ?площад|торгов(?:ое|ое помещ)|warehouse|(?<![а-яёіїґ])склад(?!н|ыв)|производствен(?:ное|ых) ?помещ|spatiu comercial|(?:închiriez|inchiriez|vând|vand)\s+birou|birou\s+de\s+(?:închiriat|inchiriat|vânzare|vanzare)|\d+\s*sot(?:ix|ka)|\d+\s*сот(?:ок|ка|ки|ых)|yer\s*maydoni|bosh\s*yer|уч[аа]сток\s*земл|servis\s*uchun|kassaprav|avtomoyka|автомойк|car\s?wash|шиномонтаж|салон\s*красот|zallik\s*saloni|beauty\s*salon|beauty[\s-]?кабинет|космет(?:ическ|олог)[а-яё]*\s*кабинет|массажн[а-яё]*\s*кабинет|маникюрн[а-яё]*\s*(?:кабинет|студи)|nail\s*(?:bar|studio|salon)|аренда\s+(?:beauty\s+)?кабинет|парикмахерск|барбершоп|barbershop|аренда\s+рабоч(?:его|ее)\s*мест|рабоч(?:ее|его)\s*мест[оае]\s+(?:мастер|для\s+мастер|под\s+)|оренд[аи]\s+робоч(?:ого|е)\s*місц|аренда\s+гараж|гараж[ае]?\s+(?:аренд|сда[её]тся|ijara)|\bgaraj\b|\bgarage\b|o[\u2018\u2019\u02bb\u02bc'`ʻʼ]?quv\s*xona|o[\u2018\u2019\u02bb\u02bc'`ʻʼ]?quv\s*markaz|учебн(?:ый|ое|ая|ого)\s*(?:класс|помещ|кабинет|центр)|под\s+(?:бар|каф[её]|ресторан|магазин|салон|склад|бизнес|спортпит|спорт\s*пит|аптек|пекарн|пункт|шоурум|showroom|офис)|шоурум|showroom|спортпит|sportpit|\bsklad(?:lar)?\b|\bombor(?:xona)?\b|(?:avto\s*)?ser?vis\s+(?:arenda|ijara|beriladi)|авто\s?сервис\s+(?:аренд|сда[её]тся)|детейлинг|detailing|^\s*парковк|avtoturargoh|sot(?:ish|uv)\w*\s*joy|savdo\s*(?:uchun|joy)|(?:ijara\w*|arenda\w*|sot(?:ish|uv)\w*)\s+do[\u2018\u2019'`ʻʼ]?kon|do[\u2018\u2019'`ʻʼ]?kon\s+(?:ijara\w*|arenda\w*|sot(?:ish|uv)\w*)|(?:аренд|сда[её]т|сдам|прода|продаж)[а-яё]*\s+(?:(?:в|под|аренду|готов[а-яё]*|действующ[а-яё]*|срочно|отдельн[а-яё]*|стоящ[а-яё]*|цел[а-яё]*)\s+){0,3}(?:здани|каф[её])|бутик|[|,/]\s*(?:салон|бутик|аптек)|arenda\w*\s+joy|ijara\w*\s+joy|готов[а-яё]*\s+бизнес|tayyor\s+biznes|бизнес\s+(?:ресторан|каф[её]|магазин)|(?:сда[её]тся|аренда|прода[её]тся)\s+(?:готов[а-яё]*\s+)?(?:ресторан|каф[её]|чайхан|choyxona)|marojni|muzqaymoq)/i;
|
|
6
6
|
|
|
7
7
|
export function looksCommercialHousing(value) {
|
|
8
8
|
return value ? COMMERCIAL_HOUSING_RE.test(String(value)) : false;
|
package/src/housing-intent.js
CHANGED
|
@@ -9,6 +9,29 @@ const UZ_CONTEXTUAL_RENT_OUT_RE = /(?:^|[^\p{L}\p{N}_])(?:ijaraga|ижарага
|
|
|
9
9
|
const UZ_PER_DAY_RE = /(?:^|[^\p{L}\p{N}_])(?:kuniga|кунига)(?=$|[^\p{L}\p{N}_])/iu;
|
|
10
10
|
const UZ_DAILY_RENT_PRICE_RE = /(?:narx|нарх|ijara|ижара|to['’`]?lov|т[ўу]лов|оплата)[^.!?\r\n]{0,48}(?:kuniga|кунига)|(?:kuniga|кунига)[^.!?\r\n]{0,48}(?:narx|нарх|ijara|ижара|to['’`]?lov|т[ўу]лов|оплата)/iu;
|
|
11
11
|
|
|
12
|
+
// "ищу квартиру" / "шукаю квартиру" style aliases only match a literal,
|
|
13
|
+
// adjacent phrase. Real posts routinely insert a room count or adjective
|
|
14
|
+
// between the search verb and the housing noun ("Ищу 2-комнатную квартиру"),
|
|
15
|
+
// which left those posts unclassified -- neither offer nor wanted -- so they
|
|
16
|
+
// slipped past the propertyWanted filter and were shown as if someone were
|
|
17
|
+
// offering the flat. These contextual patterns allow a short bounded gap
|
|
18
|
+
// between the verb and the noun, and the gap is inspected for a "buy" marker
|
|
19
|
+
// so "ищу купить 2-комнатную квартиру" still resolves to buy, not rentIn.
|
|
20
|
+
const WANTED_GAP = '([^.!?\\r\\n]{0,40}?)';
|
|
21
|
+
const WANTED_CONTEXT_RE = Object.freeze([
|
|
22
|
+
new RegExp(`(?:^|[^\\p{L}\\p{N}_])(?:ищу|сниму|хочу\\s+снять|хочу\\s+купить|нужна|нужно|нужен)(?=$|[^\\p{L}\\p{N}_])${WANTED_GAP}(?:квартир\\p{L}*|жиль[ёе]\\p{L}*|комнат\\p{L}*)(?=$|[^\\p{L}\\p{N}_])`, 'iu'),
|
|
23
|
+
new RegExp(`(?:^|[^\\p{L}\\p{N}_])(?:шукаю|зніму|хочу\\s+зняти|хочу\\s+купити|потрібна|потрібно|потрібен)(?=$|[^\\p{L}\\p{N}_])${WANTED_GAP}(?:квартир\\p{L}*|житл\\p{L}*|кімнат\\p{L}*|будин\\p{L}*)(?=$|[^\\p{L}\\p{N}_])`, 'iu'),
|
|
24
|
+
]);
|
|
25
|
+
const WANTED_BUY_MARKER_RE = /(?:купить|покупки|покупку|купити|купівлі)/iu;
|
|
26
|
+
|
|
27
|
+
function detectContextualWantedAction(text) {
|
|
28
|
+
for (const re of WANTED_CONTEXT_RE) {
|
|
29
|
+
const match = text.match(re);
|
|
30
|
+
if (match) return WANTED_BUY_MARKER_RE.test(match[1] || '') ? 'buy' : 'rentIn';
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
12
35
|
export const HOUSING_ACTIONS = Object.freeze([
|
|
13
36
|
group('sell', {
|
|
14
37
|
ru: ['продам', 'продаю', 'продаётся', 'продается', 'выставил на продажу', 'выставила на продажу'],
|
|
@@ -112,7 +135,9 @@ export function resolveHousingIntent(value) {
|
|
|
112
135
|
if (!text.trim()) return null;
|
|
113
136
|
|
|
114
137
|
const actionMatch = findCanonical(text, HOUSING_ACTIONS, { partial: true });
|
|
115
|
-
const action = actionMatch?.canonical
|
|
138
|
+
const action = actionMatch?.canonical
|
|
139
|
+
|| (UZ_CONTEXTUAL_RENT_OUT_RE.test(text) ? 'rentOut' : null)
|
|
140
|
+
|| detectContextualWantedAction(text);
|
|
116
141
|
const durationDeal = findCanonical(text, HOUSING_DEAL_TYPES, { partial: true });
|
|
117
142
|
const hasContextualUzPerDay = UZ_PER_DAY_RE.test(text)
|
|
118
143
|
&& ((action === 'rentOut' || action === 'rentIn') || UZ_DAILY_RENT_PRICE_RE.test(text));
|