@whiteslove/parsing-lexicon 0.9.3 → 0.9.5

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.3",
3
+ "version": "0.9.5",
4
4
  "description": "Shared deterministic multilingual parsing lexicon for WhitesLove housing and hiring services",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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|склад(?!н|ыв)|производствен(?:ное|ых) ?помещ|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;
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;
@@ -63,3 +63,22 @@ export function isDirectOwner(value) {
63
63
  export function hasZeroCommissionSignal(value) {
64
64
  return isDirectOwner(value) || (Boolean(value) && EXPLICIT_ZERO_COMMISSION_RE.test(String(value)));
65
65
  }
66
+
67
+ // A Telegram group's own "welcome"/intro post routinely names the group
68
+ // itself, and a real-estate group is typically named after what it rents
69
+ // out ("Оренда квартир Одеса"). That alone can satisfy HOUSING_RE-style
70
+ // content gates even though the message describes no property at all -- it
71
+ // is a greeting, not a listing. No genuine single-property ad opens with
72
+ // "welcome to the group", so this is safe to treat as a strong, low-noise
73
+ // signal on its own.
74
+ const GROUP_WELCOME_RE = /(?:добро\s+пожаловать\s+в\s+групп\p{L}*|ласкаво\s+просимо\s+(?:до|в)\s+груп\p{L}*|welcome\s+to\s+(?:the\s+|our\s+)?group|bine\s+a[țt]i\s+venit\s+[îi]n\s+grup|guruhga\s+xush\s+kelibsiz|топқа\s+қош\s+келдіңіз)/iu;
75
+
76
+ /**
77
+ * A group's channel welcome/intro post, or an off-topic channel
78
+ * cross-promotion riding on the group's housing-flavored name -- neither
79
+ * describes a specific property and both should be excluded from listing
80
+ * feeds even when they incidentally contain housing keywords.
81
+ */
82
+ export function looksLikeGroupWelcomeMessage(value) {
83
+ return Boolean(value) && GROUP_WELCOME_RE.test(String(value));
84
+ }
@@ -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 || (UZ_CONTEXTUAL_RENT_OUT_RE.test(text) ? 'rentOut' : null);
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));