@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.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -0
  3. package/index.d.ts +375 -0
  4. package/package.json +75 -0
  5. package/src/central-asia-locations.js +211 -0
  6. package/src/central-asia.js +233 -0
  7. package/src/contact.d.ts +44 -0
  8. package/src/contact.js +159 -0
  9. package/src/countries.js +175 -0
  10. package/src/country-context.d.ts +10 -0
  11. package/src/country-context.js +22 -0
  12. package/src/currency.d.ts +14 -0
  13. package/src/currency.js +66 -0
  14. package/src/geo.js +169 -0
  15. package/src/geography-central-asia.js +42 -0
  16. package/src/geography-detection.d.ts +14 -0
  17. package/src/geography-detection.js +64 -0
  18. package/src/geography-display.d.ts +13 -0
  19. package/src/geography-display.js +113 -0
  20. package/src/geography.js +210 -0
  21. package/src/hiring-advanced.js +147 -0
  22. package/src/hiring-candidate-fields.d.ts +9 -0
  23. package/src/hiring-candidate-fields.js +92 -0
  24. package/src/hiring-context.d.ts +64 -0
  25. package/src/hiring-context.js +412 -0
  26. package/src/hiring-language-extensions.d.ts +19 -0
  27. package/src/hiring-language-extensions.js +139 -0
  28. package/src/hiring-languages.js +135 -0
  29. package/src/hiring-location-fields.d.ts +3 -0
  30. package/src/hiring-location-fields.js +53 -0
  31. package/src/hiring-professions-ro.js +191 -0
  32. package/src/hiring-professions.js +437 -0
  33. package/src/hiring-requirements.d.ts +16 -0
  34. package/src/hiring-requirements.js +170 -0
  35. package/src/hiring-salary-context.d.ts +27 -0
  36. package/src/hiring-salary-context.js +76 -0
  37. package/src/hiring-semantics.d.ts +29 -0
  38. package/src/hiring-semantics.js +218 -0
  39. package/src/hiring-skills.d.ts +21 -0
  40. package/src/hiring-skills.js +315 -0
  41. package/src/hiring-source-aliases.d.ts +32 -0
  42. package/src/hiring-source-aliases.js +186 -0
  43. package/src/hiring-source-semantics.d.ts +31 -0
  44. package/src/hiring-source-semantics.js +293 -0
  45. package/src/hiring-temporal.d.ts +15 -0
  46. package/src/hiring-temporal.js +67 -0
  47. package/src/hiring-vacancy-fields.d.ts +5 -0
  48. package/src/hiring-vacancy-fields.js +23 -0
  49. package/src/hiring-work-semantics.d.ts +34 -0
  50. package/src/hiring-work-semantics.js +72 -0
  51. package/src/hiring.js +180 -0
  52. package/src/housing-address.d.ts +18 -0
  53. package/src/housing-address.js +210 -0
  54. package/src/housing-context.d.ts +36 -0
  55. package/src/housing-context.js +183 -0
  56. package/src/housing-features.js +30 -0
  57. package/src/housing-intent.d.ts +9 -0
  58. package/src/housing-intent.js +132 -0
  59. package/src/housing-listing-fields.js +131 -0
  60. package/src/housing-money.d.ts +7 -0
  61. package/src/housing-money.js +102 -0
  62. package/src/housing-source-aliases.d.ts +8 -0
  63. package/src/housing-source-aliases.js +51 -0
  64. package/src/housing-structured.d.ts +55 -0
  65. package/src/housing-structured.js +219 -0
  66. package/src/housing.js +200 -0
  67. package/src/index.js +44 -0
  68. package/src/kz-location-extensions.js +307 -0
  69. package/src/landmarks.js +21 -0
  70. package/src/lexicon-core.js +58 -0
  71. package/src/location-merge.js +106 -0
  72. package/src/locations.js +456 -0
  73. package/src/money-core.d.ts +10 -0
  74. package/src/money-core.js +81 -0
  75. package/src/money-lexicon.d.ts +5 -0
  76. package/src/money-lexicon.js +79 -0
  77. package/src/money.js +154 -0
  78. package/src/normalization.js +384 -0
  79. package/src/odesa-metropolitan.js +145 -0
  80. package/src/romania-geography.js +47 -0
  81. package/src/tashkent-colloquial.js +72 -0
  82. package/src/tashkent-housing-geography.d.ts +38 -0
  83. package/src/tashkent-housing-geography.js +211 -0
  84. package/src/tashkent-pois.js +215 -0
  85. package/src/tashkent-residential-complexes.js +272 -0
  86. package/src/ua-location-extensions-major.js +118 -0
  87. package/src/ua-location-extensions-metro.js +9 -0
  88. package/src/ua-location-extensions-regional.js +246 -0
  89. package/src/ua-secondary-cities.d.ts +2 -0
  90. package/src/ua-secondary-cities.js +57 -0
  91. package/src/ukraine.js +75 -0
  92. package/src/uz-location-extensions.js +259 -0
@@ -0,0 +1,183 @@
1
+ import { lexiconEntity, deepFreeze } from './lexicon-core.js';
2
+ import { findAllCanonical, findCanonical } from './normalization.js';
3
+
4
+ const group = (canonical, aliases, extra = {}) => lexiconEntity(canonical, aliases, extra);
5
+ const first = (text, entries) => findCanonical(text, entries, { partial: true })?.canonical || null;
6
+ const all = (text, entries) => [...new Set(findAllCanonical(text, entries).map(({ canonical }) => canonical).filter(Boolean))];
7
+
8
+ export const PROPERTY_CONDITION_TERMS = Object.freeze([
9
+ group('newRenovation', { ru: ['новый ремонт', 'свежий ремонт', 'после ремонта'], en: ['new renovation', 'freshly renovated', 'recently renovated'], uk: ['новий ремонт', 'свіжий ремонт', 'після ремонту'], ro: ['renovare nouă', 'proaspăt renovat'], uzLatn: ['yangi remont', 'yangi ta’mir'], uzCyrl: ['янги ремонт', 'янги таъмир'], kk: ['жаңа жөндеу'] }),
10
+ group('cosmeticRenovation', { ru: ['косметический ремонт'], en: ['cosmetic renovation'], uk: ['косметичний ремонт'], ro: ['renovare cosmetică'], uzLatn: ['kosmetik remont'], uzCyrl: ['косметик ремонт'], kk: ['косметикалық жөндеу'] }),
11
+ group('euroRenovation', { ru: ['евроремонт'], en: ['euro renovation', 'euro-renovated'], uk: ['євроремонт'], ro: ['renovare euro'], uzLatn: ['evroremont'], uzCyrl: ['евроремонт'], kk: ['евроремонт'] }),
12
+ group('designerRenovation', { ru: ['дизайнерский ремонт'], en: ['designer renovation', 'designer interior'], uk: ['дизайнерський ремонт'], ro: ['renovare de designer'], uzLatn: ['dizaynerlik remont'], uzCyrl: ['дизайнерлик ремонт'], kk: ['дизайнерлік жөндеу'] }),
13
+ group('good', { ru: ['хорошее состояние', 'отличное состояние'], en: ['good condition', 'excellent condition'], uk: ['хороший стан', 'відмінний стан'], ro: ['stare bună', 'stare excelentă'], uzLatn: ['yaxshi holat'], uzCyrl: ['яхши ҳолат'], kk: ['жақсы жағдай'] }),
14
+ group('livable', { ru: ['жилое состояние'], en: ['livable condition', 'habitable'], uk: ['житловий стан'], ro: ['locuibil'], uzLatn: ['yashashga tayyor'], uzCyrl: ['яшашга тайёр'], kk: ['тұруға жарамды'] }),
15
+ group('needsRenovation', { ru: ['требует ремонта', 'под ремонт', 'нужен ремонт'], en: ['needs renovation', 'requires renovation'], uk: ['потребує ремонту', 'під ремонт'], ro: ['necesită renovare'], uzLatn: ['remont kerak'], uzCyrl: ['ремонт керак'], kk: ['жөндеу қажет'] }),
16
+ group('shell', { ru: ['черновая отделка', 'черновая', 'без отделки'], en: ['shell condition', 'shell and core', 'unfinished shell'], uk: ['чорнова обробка', 'без оздоблення'], ro: ['la roșu', 'fără finisaje'], uzLatn: ['qora suvoq', 'qora suvoq holati'], uzCyrl: ['қора сувоқ'], kk: ['қара әрлеу'] }),
17
+ group('whiteBox', { ru: ['предчистовая', 'предчистовая отделка'], en: ['white box', 'white-box'], uk: ['передчистове оздоблення'], ro: ['semifinisat'], uzLatn: ['white box'], uzCyrl: ['white box'], kk: ['white box'] }),
18
+ ]);
19
+
20
+ export const LAYOUT_TERMS = Object.freeze([
21
+ group('separateRooms', { ru: ['раздельные комнаты', 'изолированные комнаты'], en: ['separate rooms', 'isolated rooms'], uk: ['роздільні кімнати', 'ізольовані кімнати'], ro: ['camere separate'], uzLatn: ['alohida xonalar'], uzCyrl: ['алоҳида хоналар'], kk: ['бөлек бөлмелер'] }),
22
+ group('adjacentRooms', { ru: ['смежные комнаты', 'проходные комнаты', 'проходная комната'], en: ['adjacent rooms', 'walk-through room'], uk: ['суміжні кімнати', 'прохідна кімната'], ro: ['camere decomandate parțial', 'cameră de trecere'], uzLatn: ['o‘tish xonasi'], uzCyrl: ['ўтиш хонаси'], kk: ['өтпелі бөлме'] }),
23
+ group('kitchenStudio', { ru: ['кухня-студия', 'кухня студия'], en: ['kitchen studio', 'open kitchen living room'], uk: ['кухня-студія'], ro: ['bucătărie open space'], uzLatn: ['oshxona studiya'], uzCyrl: ['ошхона студия'], kk: ['асүй-студия'] }),
24
+ group('euroTwoRoom', { ru: ['евродвушка', 'евро двушка'], en: ['euro two-room', 'euro one bedroom'], uk: ['євродвушка'], ro: ['apartament euro două camere'], uzLatn: ['evro ikki xonali'], uzCyrl: ['евро икки хонали'], kk: ['еуро екі бөлмелі'] }),
25
+ group('euroThreeRoom', { ru: ['евротрёшка', 'евротрешка', 'евро трешка'], en: ['euro three-room', 'euro two bedroom'], uk: ['євротрійка'], ro: ['apartament euro trei camere'], uzLatn: ['evro uch xonali'], uzCyrl: ['евро уч хонали'], kk: ['еуро үш бөлмелі'] }),
26
+ group('openPlan', { ru: ['свободная планировка'], en: ['open plan', 'free layout'], uk: ['вільне планування'], ro: ['plan deschis'], uzLatn: ['erkin reja'], uzCyrl: ['эркин режа'], kk: ['еркін жоспарлау'] }),
27
+ group('masterBedroom', { ru: ['мастер-спальня', 'мастер спальня'], en: ['master bedroom'], uk: ['майстер-спальня'], ro: ['dormitor matrimonial'], uzLatn: ['master yotoqxona'], uzCyrl: ['мастер ётоқхона'], kk: ['master bedroom'] }),
28
+ group('walkInCloset', { ru: ['гардеробная'], en: ['walk-in closet', 'dressing room'], uk: ['гардеробна'], ro: ['dressing'], uzLatn: ['garderob xona'], uzCyrl: ['гардероб хона'], kk: ['киім бөлмесі'] }),
29
+ ]);
30
+
31
+ export const BUILDING_TYPE_TERMS = Object.freeze([
32
+ group('brick', { ru: ['кирпичный дом', 'кирпич'], en: ['brick building', 'brick house'], uk: ['цегляний будинок'], ro: ['clădire din cărămidă'], uzLatn: ['g‘ishtli uy', "g'ishtli uy"], uzCyrl: ['ғиштли уй'], kk: ['кірпіш үй'] }),
33
+ group('panel', { ru: ['панельный дом', 'панелька'], en: ['panel building', 'prefab panel'], uk: ['панельний будинок'], ro: ['bloc din panouri'], uzLatn: ['panelli uy'], uzCyrl: ['панелли уй'], kk: ['панельді үй'] }),
34
+ group('monolithic', { ru: ['монолитный дом', 'монолит'], en: ['monolithic building', 'monolith'], uk: ['монолітний будинок'], ro: ['clădire monolit'], uzLatn: ['monolit uy'], uzCyrl: ['монолит уй'], kk: ['монолитті үй'] }),
35
+ group('monolithicBrick', { ru: ['монолитно-кирпичный', 'монолит кирпич'], en: ['monolithic brick'], uk: ['монолітно-цегляний'], ro: ['monolit-cărămidă'], uzLatn: ['monolit g‘isht'], uzCyrl: ['монолит ғишт'], kk: ['монолит кірпіш'] }),
36
+ group('block', { ru: ['блочный дом'], en: ['block building'], uk: ['блочний будинок'], ro: ['clădire din blocuri'], uzLatn: ['blokli uy'], uzCyrl: ['блокли уй'], kk: ['блокты үй'] }),
37
+ group('frame', { ru: ['каркасный дом'], en: ['frame building'], uk: ['каркасний будинок'], ro: ['clădire pe cadre'], uzLatn: ['karkasli uy'], uzCyrl: ['каркасли уй'], kk: ['қаңқалы үй'] }),
38
+ ]);
39
+
40
+ export const BUILDING_STATUS_TERMS = Object.freeze([
41
+ group('newBuilding', { ru: ['новостройка', 'новый дом'], en: ['new build', 'new building'], uk: ['новобудова', 'новий будинок'], ro: ['construcție nouă'], uzLatn: ['yangi qurilish'], uzCyrl: ['янги қурилиш'], kk: ['жаңа құрылыс'] }),
42
+ group('secondary', { ru: ['вторичка', 'вторичный рынок'], en: ['secondary market', 'resale property'], uk: ['вторинний ринок'], ro: ['piața secundară'], uzLatn: ['ikkilamchi bozor'], uzCyrl: ['иккиламчи бозор'], kk: ['қайталама нарық'] }),
43
+ group('underConstruction', { ru: ['строящийся дом', 'строится', 'на этапе строительства'], en: ['under construction'], uk: ['будується', 'на етапі будівництва'], ro: ['în construcție'], uzLatn: ['qurilmoqda'], uzCyrl: ['қурилмоқда'], kk: ['салынып жатыр'] }),
44
+ group('commissioned', { ru: ['дом сдан', 'сдан в эксплуатацию', 'готовый дом'], en: ['commissioned', 'completed building', 'ready building'], uk: ['будинок зданий', 'введено в експлуатацію'], ro: ['dat în folosință', 'finalizat'], uzLatn: ['uy topshirilgan'], uzCyrl: ['уй топширилган'], kk: ['үй пайдалануға берілді'] }),
45
+ ]);
46
+
47
+ export const PROPERTY_PRICE_CONTEXT = Object.freeze([
48
+ group('totalPrice', { ru: ['за квартиру', 'за весь объект', 'общая цена'], en: ['total price', 'for the apartment', 'for the whole property'], uk: ['за квартиру', 'за весь об’єкт'], ro: ['preț total'], uzLatn: ['butun uy uchun'], uzCyrl: ['бутун уй учун'], kk: ['пәтердің толық бағасы'] }),
49
+ group('pricePerSqm', { ru: ['за квадрат', 'за м²', 'за м2', 'цена за метр'], en: ['per sqm', 'per m2', 'per square meter'], uk: ['за м²', 'за квадрат'], ro: ['pe mp', 'pe m²'], uzLatn: ['m2 uchun', 'kvadrat metr uchun'], uzCyrl: ['м2 учун'], kk: ['м² үшін'] }),
50
+ group('pricePerMonth', { ru: ['в месяц', 'за месяц', 'ежемесячно'], en: ['per month', 'monthly'], uk: ['на місяць', 'щомісяця'], ro: ['pe lună', 'lunar'], uzLatn: ['oyiga'], uzCyrl: ['ойига'], kk: ['айына'] }),
51
+ group('pricePerDay', { ru: ['в сутки', 'за сутки', 'за ночь'], en: ['per day', 'daily', 'per night'], uk: ['за добу', 'за ніч'], ro: ['pe zi', 'pe noapte'], uzLatn: ['kuniga', 'sutkaga'], uzCyrl: ['кунига', 'суткага'], kk: ['тәулігіне', 'күніне'] }),
52
+ group('pricePerHour', { ru: ['за час', 'почасово'], en: ['per hour', 'hourly'], uk: ['за годину', 'погодинно'], ro: ['pe oră'], uzLatn: ['soatiga'], uzCyrl: ['соатига'], kk: ['сағатына'] }),
53
+ ]);
54
+
55
+ export const PRICE_MODIFIERS = Object.freeze([
56
+ group('negotiable', { ru: ['торг', 'торг уместен', 'торг возможен', 'разумный торг'], en: ['negotiable', 'offers considered'], uk: ['торг', 'можливий торг'], ro: ['negociabil'], uzLatn: ['kelishamiz', 'narxi kelishiladi'], uzCyrl: ['келишамиз', 'нархи келишилади'], kk: ['сауда бар', 'келісуге болады'] }),
57
+ group('fixed', { ru: ['без торга', 'цена окончательная', 'цена фиксированная'], en: ['fixed price', 'non-negotiable'], uk: ['без торгу', 'ціна остаточна'], ro: ['preț fix', 'nenegociabil'], uzLatn: ['narx qat’iy'], uzCyrl: ['нарх қатъий'], kk: ['баға тұрақты'] }),
58
+ group('reduced', { ru: ['цена снижена', 'ниже рынка'], en: ['reduced price', 'below market'], uk: ['ціну знижено', 'нижче ринку'], ro: ['preț redus', 'sub prețul pieței'], uzLatn: ['narx tushirildi'], uzCyrl: ['нарх туширилди'], kk: ['баға төмендетілді'] }),
59
+ group('urgent', { ru: ['срочно', 'срочная продажа', 'горящая цена'], en: ['urgent sale', 'urgent'], uk: ['терміново', 'терміновий продаж'], ro: ['urgent', 'vânzare urgentă'], uzLatn: ['shoshilinch', 'tez sotiladi'], uzCyrl: ['шошилинч'], kk: ['шұғыл'] }),
60
+ ]);
61
+
62
+ export const RENT_DURATION_TERMS = Object.freeze([
63
+ group('hourly', { ru: ['почасово', 'на час'], en: ['hourly', 'per hour'], uk: ['погодинно'], ro: ['pe oră'], uzLatn: ['soatlik'], uzCyrl: ['соатлик'], kk: ['сағаттық'] }),
64
+ group('daily', { ru: ['посуточно', 'на сутки'], en: ['daily', 'per day'], uk: ['подобово', 'на добу'], ro: ['pe zi'], uzLatn: ['kunlik', 'sutkalik'], uzCyrl: ['кунлик', 'суткалик'], kk: ['тәуліктік', 'күндік'] }),
65
+ group('monthly', { ru: ['помесячно'], en: ['monthly'], uk: ['помісячно'], ro: ['lunar'], uzLatn: ['oylik'], uzCyrl: ['ойлик'], kk: ['ай сайын'] }),
66
+ group('shortTerm', { ru: ['краткосрочно', 'на несколько месяцев'], en: ['short term'], uk: ['короткостроково'], ro: ['pe termen scurt'], uzLatn: ['qisqa muddatga'], uzCyrl: ['қисқа муддатга'], kk: ['қысқа мерзімге'] }),
67
+ group('longTerm', { ru: ['долгосрочно', 'на длительный срок', 'минимум на год'], en: ['long term', 'long-term'], uk: ['довгостроково'], ro: ['pe termen lung'], uzLatn: ['uzoq muddatga'], uzCyrl: ['узоқ муддатга'], kk: ['ұзақ мерзімге'] }),
68
+ group('fixedTerm', { ru: ['до конца года', 'до сентября'], en: ['until the end of the year', 'until september'], uk: ['до кінця року'], ro: ['până la sfârșitul anului'], uzLatn: ['yil oxirigacha'], uzCyrl: ['йил охиригача'], kk: ['жыл соңына дейін'] }),
69
+ ]);
70
+
71
+ export const FLOOR_CONSTRAINT_TERMS = Object.freeze([
72
+ group('notFirst', { ru: ['не первый этаж', 'не 1 этаж'], en: ['not first floor'], uk: ['не перший поверх'], ro: ['nu primul etaj'], uzLatn: ['birinchi qavat emas'], uzCyrl: ['биринчи қават эмас'], kk: ['бірінші қабат емес'] }),
73
+ group('notLast', { ru: ['не последний этаж'], en: ['not top floor', 'not last floor'], uk: ['не останній поверх'], ro: ['nu ultimul etaj'], uzLatn: ['oxirgi qavat emas'], uzCyrl: ['охирги қават эмас'], kk: ['соңғы қабат емес'] }),
74
+ group('first', { ru: ['первый этаж', '1 этаж'], en: ['first floor'], uk: ['перший поверх'], ro: ['primul etaj'], uzLatn: ['birinchi qavat'], uzCyrl: ['биринчи қават'], kk: ['бірінші қабат'] }),
75
+ group('last', { ru: ['последний этаж'], en: ['top floor', 'last floor'], uk: ['останній поверх'], ro: ['ultimul etaj'], uzLatn: ['oxirgi qavat'], uzCyrl: ['охирги қават'], kk: ['соңғы қабат'] }),
76
+ group('basement', { ru: ['цоколь', 'подвал'], en: ['basement'], uk: ['цоколь', 'підвал'], ro: ['demisol', 'subsol'], uzLatn: ['podval'], uzCyrl: ['подвал'], kk: ['жертөле'] }),
77
+ group('attic', { ru: ['мансарда'], en: ['attic', 'mansard'], uk: ['мансарда'], ro: ['mansardă'], uzLatn: ['mansarda'], uzCyrl: ['мансарда'], kk: ['мансарда'] }),
78
+ ]);
79
+
80
+ export const FURNITURE_STATE_TERMS = Object.freeze([
81
+ group('full', { ru: ['полностью меблирована', 'вся мебель', 'с мебелью'], en: ['fully furnished', 'furnished'], uk: ['повністю мебльована', 'з меблями'], ro: ['complet mobilat', 'mobilat'], uzLatn: ['to‘liq mebelli', "to'liq mebelli"], uzCyrl: ['тўлиқ мебелли'], kk: ['толық жиһаздалған'] }),
82
+ group('partial', { ru: ['частично меблирована', 'частично с мебелью'], en: ['partly furnished', 'partially furnished'], uk: ['частково мебльована'], ro: ['parțial mobilat'], uzLatn: ['qisman mebelli'], uzCyrl: ['қисман мебелли'], kk: ['ішінара жиһаздалған'] }),
83
+ group('none', { ru: ['без мебели', 'пустая квартира'], en: ['unfurnished', 'without furniture'], uk: ['без меблів'], ro: ['nemobilat'], uzLatn: ['mebelsiz'], uzCyrl: ['мебелсиз'], kk: ['жиһазсыз'] }),
84
+ ]);
85
+
86
+ export const PET_POLICY_TERMS = Object.freeze([
87
+ group('notAllowed', { ru: ['без животных', 'с животными нельзя', 'животных нельзя'], en: ['no pets', 'pets not allowed'], uk: ['без тварин', 'з тваринами не можна'], ro: ['fără animale', 'animalele nu sunt permise'], uzLatn: ['hayvonsiz', 'uy hayvonlari mumkin emas'], uzCyrl: ['ҳайвонсиз'], kk: ['жануарларсыз', 'үй жануарларына болмайды'] }),
88
+ group('conditional', { ru: ['животные по согласованию', 'с животными по договоренности'], en: ['pets negotiable', 'pets by agreement'], uk: ['тварини за домовленістю'], ro: ['animale cu acord'], uzLatn: ['hayvonlar kelishuv asosida'], uzCyrl: ['ҳайвонлар келишув асосида'], kk: ['жануарлар келісім бойынша'] }),
89
+ group('allowed', { ru: ['с животными можно', 'можно с котом', 'можно с собакой'], en: ['pets allowed', 'pet friendly'], uk: ['з тваринами можна'], ro: ['animale permise'], uzLatn: ['hayvon bilan mumkin'], uzCyrl: ['ҳайвон билан мумкин'], kk: ['үй жануарларымен болады'] }),
90
+ ]);
91
+
92
+ export const CHILD_POLICY_TERMS = Object.freeze([
93
+ group('notAllowed', { ru: ['без детей', 'с детьми нельзя'], en: ['no children', 'children not allowed'], uk: ['без дітей', 'з дітьми не можна'], ro: ['fără copii'], uzLatn: ['bolalarsiz'], uzCyrl: ['болаларсиз'], kk: ['баласыз'] }),
94
+ group('allowed', { ru: ['с детьми можно', 'можно с детьми'], en: ['children allowed', 'families with children welcome'], uk: ['з дітьми можна'], ro: ['copii acceptați'], uzLatn: ['bolalar bilan mumkin'], uzCyrl: ['болалар билан мумкин'], kk: ['балалармен болады'] }),
95
+ ]);
96
+
97
+ export const SMOKING_POLICY_TERMS = Object.freeze([
98
+ group('notAllowed', { ru: ['курить нельзя', 'не курить', 'некурящим'], en: ['no smoking', 'smoking not allowed'], uk: ['палити не можна'], ro: ['fumatul interzis'], uzLatn: ['chekish mumkin emas'], uzCyrl: ['чекиш мумкин эмас'], kk: ['темекі шегуге болмайды'] }),
99
+ group('allowed', { ru: ['курить можно'], en: ['smoking allowed'], uk: ['палити можна'], ro: ['fumat permis'], uzLatn: ['chekish mumkin'], uzCyrl: ['чекиш мумкин'], kk: ['темекі шегуге болады'] }),
100
+ ]);
101
+
102
+ export const DOCUMENT_STATUS_TERMS = Object.freeze([
103
+ group('cadastralReady', { ru: ['кадастр есть', 'кадастр готов', 'кадастровые документы готовы'], en: ['cadastral documents ready', 'cadastre ready'], uk: ['кадастр готовий'], ro: ['cadastru gata'], uzLatn: ['kadastr bor', 'kadastr tayyor'], uzCyrl: ['кадастр бор', 'кадастр тайёр'], kk: ['кадастр дайын'] }),
104
+ group('ownership', { ru: ['право собственности', 'документы на руках'], en: ['ownership title', 'title deed'], uk: ['право власності'], ro: ['drept de proprietate'], uzLatn: ['mulk huquqi'], uzCyrl: ['мулк ҳуқуқи'], kk: ['меншік құқығы'] }),
105
+ group('noEncumbrance', { ru: ['без обременений', 'обременений нет'], en: ['no encumbrances', 'free of encumbrances'], uk: ['без обтяжень'], ro: ['fără sarcini'], uzLatn: ['taqiqsiz'], uzCyrl: ['тақиқсиз'], kk: ['ауыртпалықсыз'] }),
106
+ group('encumbered', { ru: ['в залоге', 'под арестом', 'есть обременение'], en: ['encumbered', 'under lien'], uk: ['в заставі', 'є обтяження'], ro: ['cu sarcini'], uzLatn: ['garovda'], uzCyrl: ['гаровда'], kk: ['кепілде'] }),
107
+ group('nobodyRegistered', { ru: ['никто не прописан'], en: ['nobody registered'], uk: ['ніхто не зареєстрований'], ro: ['nimeni înregistrat'], uzLatn: ['hech kim ro‘yxatda emas'], uzCyrl: ['ҳеч ким рўйхатда эмас'], kk: ['ешкім тіркелмеген'] }),
108
+ ]);
109
+
110
+ export const FINANCING_TERMS = Object.freeze([
111
+ group('mortgageAllowed', { ru: ['ипотека возможна', 'подходит под ипотеку'], en: ['mortgage available', 'mortgage eligible'], uk: ['іпотека можлива'], ro: ['credit ipotecar posibil'], uzLatn: ['ipoteka mumkin'], uzCyrl: ['ипотека мумкин'], kk: ['ипотека болады'] }),
112
+ group('mortgageNotAllowed', { ru: ['не ипотека', 'ипотека невозможна', 'только наличные'], en: ['no mortgage', 'cash only'], uk: ['іпотека неможлива', 'лише готівка'], ro: ['fără ipotecă', 'doar numerar'], uzLatn: ['ipoteka yo‘q', 'faqat naqd'], uzCyrl: ['ипотека йўқ', 'фақат нақд'], kk: ['ипотека жоқ', 'тек қолма-қол'] }),
113
+ group('installment', { ru: ['рассрочка', 'беспроцентная рассрочка'], en: ['installment', 'payment plan'], uk: ['розстрочка'], ro: ['rate', 'plată în rate'], uzLatn: ['bo‘lib to‘lash'], uzCyrl: ['бўлиб тўлаш'], kk: ['бөліп төлеу'] }),
114
+ ]);
115
+
116
+ export const LOCATION_RELATIONS = Object.freeze([
117
+ group('near', { ru: ['рядом с', 'возле', 'около'], en: ['near', 'next to', 'close to'], uk: ['поруч', 'біля'], ro: ['lângă', 'aproape de'], uzLatn: ['yaqinida', 'yonida'], uzCyrl: ['яқинида', 'ёнида'], kk: ['жанында', 'маңында'] }),
118
+ group('opposite', { ru: ['напротив', 'через дорогу'], en: ['opposite', 'across the road'], uk: ['навпроти', 'через дорогу'], ro: ['vizavi', 'peste drum'], uzLatn: ['ro‘parasida'], uzCyrl: ['рўпарасида'], kk: ['қарсысында'] }),
119
+ group('behind', { ru: ['за парком', 'за рынком', 'за базаром'], en: ['behind'], uk: ['за парком'], ro: ['în spatele'], uzLatn: ['orqasida'], uzCyrl: ['орқасида'], kk: ['артында'] }),
120
+ group('intersection', { ru: ['на пересечении', 'на перекрёстке', 'перекрёсток'], en: ['at the intersection', 'crossroads'], uk: ['на перехресті'], ro: ['la intersecție'], uzLatn: ['chorrahada'], uzCyrl: ['чорраҳада'], kk: ['қиылыста'] }),
121
+ group('walkingDistance', { ru: ['минут пешком', 'пешая доступность'], en: ['minutes walk', 'walking distance'], uk: ['хвилин пішки'], ro: ['minute pe jos'], uzLatn: ['daqiqa piyoda'], uzCyrl: ['дақиқа пиёда'], kk: ['минут жаяу'] }),
122
+ group('drivingDistance', { ru: ['минут на машине', 'минут на авто'], en: ['minutes by car', 'minutes drive'], uk: ['хвилин машиною'], ro: ['minute cu mașina'], uzLatn: ['daqiqa mashinada'], uzCyrl: ['дақиқа машинада'], kk: ['минут көлікпен'] }),
123
+ ]);
124
+
125
+ export const AVAILABILITY_TERMS = Object.freeze([
126
+ group('availableNow', { ru: ['свободна', 'свободно', 'можно заезжать', 'готова к заселению'], en: ['available now', 'ready to move in', 'move in now'], uk: ['вільна', 'можна заселятися'], ro: ['disponibil acum'], uzLatn: ['hozir bo‘sh', 'ko‘chib kirish mumkin'], uzCyrl: ['ҳозир бўш'], kk: ['қазір бос', 'кіруге болады'] }),
127
+ group('availableFrom', { ru: ['с 1 сентября', 'будет свободна с', 'освободится'], en: ['available from', 'move-in from'], uk: ['вільна з'], ro: ['disponibil din'], uzLatn: ['dan boshlab bo‘sh'], uzCyrl: ['дан бошлаб бўш'], kk: ['бастап бос'] }),
128
+ ]);
129
+
130
+ export const LISTING_STATUS_TERMS = Object.freeze([
131
+ group('sold', { ru: ['продано', 'уже продали'], en: ['sold'], uk: ['продано'], ro: ['vândut'], uzLatn: ['sotildi'], uzCyrl: ['сотилди'], kk: ['сатылды'] }),
132
+ group('rented', { ru: ['сдано', 'уже сдали', 'уже сдана'], en: ['rented', 'already rented'], uk: ['здано', 'вже здали'], ro: ['închiriat'], uzLatn: ['ijaraga berildi'], uzCyrl: ['ижарага берилди'], kk: ['жалға берілді'] }),
133
+ group('reserved', { ru: ['бронь', 'забронировано', 'в резерве'], en: ['reserved', 'booked'], uk: ['бронь', 'заброньовано'], ro: ['rezervat'], uzLatn: ['bron qilingan'], uzCyrl: ['брон қилинган'], kk: ['броньдалған'] }),
134
+ group('closed', { ru: ['не актуально', 'неактуально', 'снято с продажи', 'объявление закрыто'], en: ['not available', 'listing closed', 'withdrawn'], uk: ['не актуально', 'знято з продажу'], ro: ['nu mai este disponibil'], uzLatn: ['aktual emas'], uzCyrl: ['актуал эмас'], kk: ['өзекті емес'] }),
135
+ group('outdated', { ru: ['архив', 'архивное объявление'], en: ['archive', 'archived listing'], uk: ['архів'], ro: ['arhivă'], uzLatn: ['arxiv'], uzCyrl: ['архив'], kk: ['мұрағат'] }),
136
+ group('active', { ru: ['актуально', 'ещё актуально', 'еще актуально'], en: ['available', 'still available', 'active'], uk: ['актуально', 'ще актуально'], ro: ['disponibil'], uzLatn: ['aktual'], uzCyrl: ['актуал'], kk: ['өзекті'] }),
137
+ ]);
138
+
139
+ const statusPriority = Object.freeze(['sold', 'rented', 'reserved', 'closed', 'outdated', 'active']);
140
+
141
+ function resolveFloorConstraints(text) {
142
+ const values = all(text, FLOOR_CONSTRAINT_TERMS);
143
+ const out = new Set(values);
144
+ if (/(?:не\s+(?:первый|1)|not\s+first|не\s+перший|nu\s+primul|birinchi\s+qavat\s+emas|бірінші\s+қабат\s+емес)[^.!?\n]{0,32}(?:этаж|floor|поверх|etaj|qavat|қабат)/iu.test(text)) out.add('notFirst');
145
+ if (out.has('notFirst')) out.delete('first');
146
+ if (out.has('notLast')) out.delete('last');
147
+ return [...out];
148
+ }
149
+
150
+ export function parseHousingContext(value) {
151
+ const text = String(value || '');
152
+ if (!text.trim()) return deepFreeze({
153
+ condition: null, layouts: [], buildingType: null, buildingStatus: null,
154
+ priceContext: null, priceModifiers: [], rentDuration: null, floorConstraints: [],
155
+ furniture: null, tenantPolicies: { pets: null, children: null, smoking: null },
156
+ documents: [], financing: [], locationRelations: [], availability: null, listingStatus: null,
157
+ });
158
+
159
+ const statuses = all(text, LISTING_STATUS_TERMS);
160
+ const listingStatus = statusPriority.find((status) => statuses.includes(status)) || null;
161
+
162
+ return deepFreeze({
163
+ condition: first(text, PROPERTY_CONDITION_TERMS),
164
+ layouts: all(text, LAYOUT_TERMS),
165
+ buildingType: first(text, BUILDING_TYPE_TERMS),
166
+ buildingStatus: first(text, BUILDING_STATUS_TERMS),
167
+ priceContext: first(text, PROPERTY_PRICE_CONTEXT),
168
+ priceModifiers: all(text, PRICE_MODIFIERS),
169
+ rentDuration: first(text, RENT_DURATION_TERMS),
170
+ floorConstraints: resolveFloorConstraints(text),
171
+ furniture: first(text, FURNITURE_STATE_TERMS),
172
+ tenantPolicies: {
173
+ pets: first(text, PET_POLICY_TERMS),
174
+ children: first(text, CHILD_POLICY_TERMS),
175
+ smoking: first(text, SMOKING_POLICY_TERMS),
176
+ },
177
+ documents: all(text, DOCUMENT_STATUS_TERMS),
178
+ financing: all(text, FINANCING_TERMS),
179
+ locationRelations: all(text, LOCATION_RELATIONS),
180
+ availability: first(text, AVAILABILITY_TERMS),
181
+ listingStatus,
182
+ });
183
+ }
@@ -0,0 +1,30 @@
1
+ import { deepFreeze } from './lexicon-core.js';
2
+ import { normalizeUnicode } from './normalization.js';
3
+
4
+ const has = (text, re) => re.test(text);
5
+ const TOKEN_LEFT = String.raw`(?:^|[^\p{L}\p{N}_])`;
6
+ const TOKEN_RIGHT = String.raw`(?=$|[^\p{L}\p{N}_])`;
7
+ const tokenRe = (source) => new RegExp(`${TOKEN_LEFT}(?:${source})${TOKEN_RIGHT}`, 'iu');
8
+
9
+ export function parseHousingFeatures(value) {
10
+ const text = normalizeUnicode(value ?? '');
11
+ if (!text) return deepFreeze({
12
+ internet: null,
13
+ courtyard: null,
14
+ gazebo: null,
15
+ petsAllowed: null,
16
+ });
17
+
18
+ const internet = has(text, tokenRe(String.raw`интернет|wi[ -]?fi|роутер|router|інтернет`)) ? true : null;
19
+ const courtyard = has(text, tokenRe(String.raw`двор(?:ик|а|е|ом)?|внутренн(?:ий|его)\s+двор|подворье|двір(?:ик|і|ом)?|courtyard|yard`)) ? true : null;
20
+ const gazebo = has(text, tokenRe(String.raw`беседк(?:а|и|е|у|ой)|альтанк(?:а|и|у|ою)|gazebo`)) ? true : null;
21
+
22
+ let petsAllowed = null;
23
+ if (has(text, tokenRe(String.raw`без\s+животн\p{L}*|с\s+животн\p{L}*\s+нельзя|животн\p{L}*\s+нельзя`))) {
24
+ petsAllowed = false;
25
+ } else if (has(text, tokenRe(String.raw`можно\s+с\s+(?:(?:небольш\p{L}*|маленьк\p{L}*|средн\p{L}*)\s+)?животн\p{L}*|с\s+животн\p{L}*\s+можно|можно\s+с\s+(?:кот\p{L}*|кошк\p{L}*|собак\p{L}*|питомц\p{L}*)`))) {
26
+ petsAllowed = true;
27
+ }
28
+
29
+ return deepFreeze({ internet, courtyard, gazebo, petsAllowed });
30
+ }
@@ -0,0 +1,9 @@
1
+ export type HousingAction = 'sell' | 'buy' | 'rentOut' | 'rentIn';
2
+ export type HousingListingKind = 'propertyOffer' | 'propertyWanted';
3
+ export type HousingDealType = 'sale' | 'longRent' | 'shortRent';
4
+ export type HousingIntentResult = Readonly<{ action: HousingAction; listingKind: HousingListingKind; dealType: HousingDealType }>;
5
+ export const HOUSING_ACTIONS: readonly unknown[];
6
+ export const HOUSING_INTENT: readonly unknown[];
7
+ export const HOUSING_DEAL_TYPES: readonly unknown[];
8
+ export const HOUSING_ACTION_MAP: Readonly<Record<HousingAction, Readonly<{ listingKind: HousingListingKind; dealType: Exclude<HousingDealType, 'shortRent'> }>>>;
9
+ export function resolveHousingIntent(value: unknown): HousingIntentResult | null;
@@ -0,0 +1,132 @@
1
+ import { findCanonical } from './normalization.js';
2
+ import { lexiconEntity } from './lexicon-core.js';
3
+
4
+ const group = (canonical, aliases, extra = {}) => lexiconEntity(canonical, aliases, extra);
5
+ const KK_RENT_OUT_ALIASES = Object.freeze(['жалға беремін', 'жалға беріледі']);
6
+ const RO_SALE_OFFER_ALIASES = Object.freeze(['de vânzare', 'de vanzare']);
7
+
8
+ export const HOUSING_ACTIONS = Object.freeze([
9
+ group('sell', {
10
+ ru: ['продам', 'продаю', 'продаётся', 'продается', 'выставил на продажу', 'выставила на продажу'],
11
+ en: ['selling', 'for sale', 'selling apartment', 'selling house'],
12
+ uk: ['продам', 'продаю', 'продається'],
13
+ ro: ['vând', 'vand', ...RO_SALE_OFFER_ALIASES, 'se vinde'],
14
+ uzLatn: ['sotaman', 'sotiladi', 'sotuvga qoyildi', "sotuvga qo'yildi"],
15
+ uzCyrl: ['сотаман', 'сотилади', 'сотувга қўйилди'],
16
+ kk: ['сатамын', 'сатылады', 'сатылымға қойылды'],
17
+ }),
18
+ group('buy', {
19
+ ru: ['куплю', 'хочу купить', 'ищу купить', 'ищу для покупки', 'нужна квартира для покупки'],
20
+ en: ['want to buy', 'looking to buy', 'looking for a property to buy', 'need an apartment to buy'],
21
+ uk: ['куплю', 'хочу купити', 'шукаю купити', 'шукаю для купівлі'],
22
+ ro: ['vreau să cumpăr', 'vreau sa cumpar', 'caut să cumpăr', 'caut sa cumpar'],
23
+ uzLatn: ['sotib olaman', 'sotib olish uchun qidiryapman', 'sotib olishga uy qidiryapman'],
24
+ uzCyrl: ['сотиб оламан', 'сотиб олиш учун қидиряпман', 'сотиб олишга уй қидиряпман'],
25
+ kk: ['сатып аламын', 'сатып алуға іздеймін', 'сатып алуға пәтер іздеймін'],
26
+ }),
27
+ group('rentOut', {
28
+ ru: ['сдам', 'сдаю', 'сдаётся', 'сдается', 'сдаётся в аренду', 'сдается в аренду'],
29
+ en: ['for rent', 'renting out', 'available for rent', 'letting'],
30
+ uk: ['здам', 'здаю', 'здається', 'здається в оренду'],
31
+ ro: ['închiriez', 'inchiriez', 'de închiriat', 'de inchiriat', 'se închiriază', 'se inchiriaza'],
32
+ uzLatn: ['ijaraga beraman', 'ijaraga beriladi', 'ijaraga topshiraman'],
33
+ uzCyrl: ['ижарага бераман', 'ижарага берилади', 'ижарага топшираман'],
34
+ kk: KK_RENT_OUT_ALIASES,
35
+ }),
36
+ group('rentIn', {
37
+ ru: ['сниму', 'хочу снять', 'ищу снять', 'ищу квартиру', 'ищу жильё', 'ищу жилье', 'нужна квартира', 'нужно жильё', 'нужно жилье'],
38
+ en: ['looking to rent', 'want to rent', 'need an apartment', 'looking for an apartment', 'looking for housing'],
39
+ uk: ['зніму', 'хочу зняти', 'шукаю квартиру', 'шукаю житло', 'потрібна квартира'],
40
+ ro: ['vreau să închiriez', 'vreau sa inchiriez', 'caut să închiriez', 'caut sa inchiriez', 'caut apartament'],
41
+ uzLatn: ['ijaraga olaman', 'uy qidiryapman', 'kvartira qidiryapman', 'ijara uchun uy qidiryapman'],
42
+ uzCyrl: ['ижарага оламан', 'уй қидиряпман', 'квартира қидиряпман', 'ижара учун уй қидиряпман'],
43
+ kk: ['жалға аламын', 'пәтер іздеймін', 'үй іздеймін', 'жалға пәтер іздеймін'],
44
+ }),
45
+ ]);
46
+
47
+ export const HOUSING_INTENT = Object.freeze([
48
+ group('offer', {
49
+ ru: ['продам', 'продаю', 'продаётся', 'продается', 'сдам', 'сдаю', 'сдаётся', 'сдается'],
50
+ en: ['for sale', 'selling', 'for rent', 'renting out'],
51
+ uk: ['продам', 'продаю', 'продається', 'здам', 'здаю', 'здається'],
52
+ ro: ['vând', 'vand', 'de vânzare', 'de vanzare', 'închiriez', 'inchiriez', 'de închiriat', 'de inchiriat'],
53
+ uzLatn: ['sotaman', 'sotiladi', 'ijaraga beraman', 'ijaraga beriladi'],
54
+ uzCyrl: ['сотаман', 'сотилади', 'ижарага бераман', 'ижарага берилади'],
55
+ kk: ['сатамын', 'сатылады', 'жалға беремін', 'жалға беріледі'],
56
+ }),
57
+ group('wanted', {
58
+ ru: ['куплю', 'ищу купить', 'сниму', 'ищу квартиру', 'ищу жильё', 'ищу жилье', 'нужна квартира'],
59
+ en: ['looking to buy', 'want to buy', 'looking to rent', 'need an apartment'],
60
+ uk: ['куплю', 'шукаю купити', 'зніму', 'шукаю квартиру', 'шукаю житло'],
61
+ ro: ['vreau să cumpăr', 'vreau sa cumpar', 'vreau să închiriez', 'vreau sa inchiriez', 'caut apartament'],
62
+ uzLatn: ['sotib olaman', 'sotib olish uchun qidiryapman', 'ijaraga olaman', 'uy qidiryapman'],
63
+ uzCyrl: ['сотиб оламан', 'сотиб олиш учун қидиряпман', 'ижарага оламан', 'уй қидиряпман'],
64
+ kk: ['сатып аламын', 'сатып алуға іздеймін', 'жалға аламын', 'пәтер іздеймін'],
65
+ }),
66
+ ]);
67
+
68
+ // Deal type is transaction mechanics only. Direction is resolved by HOUSING_ACTIONS.
69
+ export const HOUSING_DEAL_TYPES = Object.freeze([
70
+ group('sale', {
71
+ ru: ['продажа', 'продать', 'покупка'],
72
+ en: ['sale', 'purchase'],
73
+ uk: ['продаж', 'продати', 'купівля'],
74
+ ro: ['vânzare', 'vanzare', ...RO_SALE_OFFER_ALIASES, 'cumpărare', 'cumparare'],
75
+ uzLatn: ['sotish', 'sotuv', 'sotib olish'],
76
+ uzCyrl: ['сотиш', 'сотув', 'сотиб олиш'],
77
+ kk: ['сату', 'сатып алу', 'сатылым'],
78
+ }),
79
+ group('longRent', {
80
+ ru: ['аренда', 'долгосрочно', 'долгосрочная аренда', 'помесячно', 'на длительный срок'],
81
+ en: ['rent', 'long term rent', 'long-term rent', 'monthly rent'],
82
+ uk: ['оренда', 'довгостроково', 'довгострокова оренда', 'помісячно'],
83
+ ro: ['închiriere', 'inchiriere', 'chirie', 'pe termen lung', 'lunar'],
84
+ uzLatn: ['ijara', 'uzoq muddatga', 'oylik ijara'],
85
+ uzCyrl: ['ижара', 'узоқ муддатга', 'ойлик ижара'],
86
+ kk: ['жалдау', 'жалға', ...KK_RENT_OUT_ALIASES, 'ұзақ мерзімге', 'айлық жалдау'],
87
+ }),
88
+ group('shortRent', {
89
+ ru: ['посуточно', 'посуточная аренда', 'на сутки', 'на час', 'почасово', 'краткосрочно'],
90
+ en: ['daily rent', 'short term', 'short-term rent', 'per day', 'hourly'],
91
+ uk: ['подобово', 'погодинно', 'на добу', 'на годину', 'короткострокова оренда'],
92
+ ro: ['regim hotelier', 'pe zi', 'zilnic', 'pe noapte', 'închiriere pe termen scurt', 'inchiriere pe termen scurt'],
93
+ uzLatn: ['kunlik', 'sutkaga', 'sutkalik', 'soatlik', 'kuniga'],
94
+ uzCyrl: ['кунлик', 'суткага', 'суткалик', 'соатлик', 'кунига'],
95
+ kk: ['тәулік', 'тәуліктік', 'тәулігіне', 'сағаттық', 'күндік'],
96
+ }),
97
+ ]);
98
+
99
+ export const HOUSING_ACTION_MAP = Object.freeze({
100
+ sell: Object.freeze({ listingKind: 'propertyOffer', dealType: 'sale' }),
101
+ buy: Object.freeze({ listingKind: 'propertyWanted', dealType: 'sale' }),
102
+ rentOut: Object.freeze({ listingKind: 'propertyOffer', dealType: 'longRent' }),
103
+ rentIn: Object.freeze({ listingKind: 'propertyWanted', dealType: 'longRent' }),
104
+ });
105
+
106
+ export function resolveHousingIntent(value) {
107
+ const text = String(value || '');
108
+ if (!text.trim()) return null;
109
+
110
+ const action = findCanonical(text, HOUSING_ACTIONS, { partial: true });
111
+ const durationDeal = findCanonical(text, HOUSING_DEAL_TYPES, { partial: true });
112
+
113
+ if (action) {
114
+ const base = HOUSING_ACTION_MAP[action.canonical];
115
+ let dealType = base.dealType;
116
+ if (durationDeal?.canonical === 'shortRent' && (action.canonical === 'rentOut' || action.canonical === 'rentIn')) {
117
+ dealType = 'shortRent';
118
+ }
119
+ return Object.freeze({
120
+ action: action.canonical,
121
+ listingKind: base.listingKind,
122
+ dealType,
123
+ });
124
+ }
125
+
126
+ if (!durationDeal) return null;
127
+ return Object.freeze({
128
+ action: null,
129
+ listingKind: null,
130
+ dealType: durationDeal.canonical,
131
+ });
132
+ }
@@ -0,0 +1,131 @@
1
+ import { deepFreeze } from './lexicon-core.js';
2
+ import { normalizeUnicode } from './normalization.js';
3
+ import { parseHousingContext } from './housing-context.js';
4
+ import { parseHousingFeatures } from './housing-features.js';
5
+
6
+ const bool = (text, positive, negative = null) => {
7
+ if (negative?.test(text)) return false;
8
+ return positive.test(text) ? true : null;
9
+ };
10
+
11
+ const number = (match, min, max) => {
12
+ const value = match ? Number(match[1]) : null;
13
+ return Number.isFinite(value) && value >= min && value <= max ? value : null;
14
+ };
15
+
16
+ function parseBedrooms(text) {
17
+ return number(text.match(/(\d+)\s*-?\s*(?:bedroom|спальн|спалень|dormitoare|dormitor|yotoq(?:xona)?|жатын)/iu), 1, 20);
18
+ }
19
+
20
+ function parseBathrooms(text) {
21
+ const match = text.match(/(\d)\s*(?:сан\s?уз(?:е)?л\p{L}*|с\/?у(?=$|[^\p{L}\p{N}_])|ванн[аы]|bathrooms?|sanuzel|hammom)/iu)
22
+ || text.match(/(?:сан\s?уз(?:е)?л\p{L}*|bathrooms?|sanuzel|hammom)\D{0,4}(\d)/iu);
23
+ return number(match, 1, 10);
24
+ }
25
+
26
+ function parseBuildingYear(text) {
27
+ const match = text.match(/(?:год(?:а)?\s+постройк\p{L}*|построен\p{L}*|рік\s+побудов\p{L}*|built|year\s+built|an\s+construc\p{L}*|qurilgan|qurilish\s+yil\p{L}*|жыл(?:ы)?\s+салынған)\D{0,12}(19\d{2}|20[0-3]\d)/iu)
28
+ || text.match(/(19\d{2}|20[0-3]\d)\s*(?:г\.?|года|р\.?|рік|йил|жыл|year)(?=$|[^\p{L}\p{N}_])/iu);
29
+ if (!match) return null;
30
+ const year = Number(match[1]);
31
+ return year >= 1900 && year <= new Date().getFullYear() + 3 ? year : null;
32
+ }
33
+
34
+ function parseMinRentTerm(text) {
35
+ const match = text.match(/(?:минимальн\p{L}*\s+(?:срок|термин)\p{L}*(?:\s+аренд\p{L}*)?|минимум|не\s+менее|від\s+|minimum(?:\s+term)?|at\s+least)\s*[:\-]?\s*(\d{1,3})\s*(дн\p{L}*|сут\p{L}*|недел\p{L}*|тижн\p{L}*|месяц\p{L}*|мес\.?|місяц\p{L}*|год\p{L}*|рік|days?|weeks?|months?|years?)/iu);
36
+ if (!match) return null;
37
+ const value = Number(match[1]);
38
+ const unitText = match[2].toLocaleLowerCase();
39
+ const unit = /дн|сут|day/u.test(unitText) ? 'day'
40
+ : /недел|тижн|week/u.test(unitText) ? 'week'
41
+ : /месяц|мес|місяц|month/u.test(unitText) ? 'month'
42
+ : 'year';
43
+ return value > 0 ? deepFreeze({ value, unit }) : null;
44
+ }
45
+
46
+ function parseAvailableFrom(text) {
47
+ const match = text.match(/(?:доступн\p{L}*\s+с|свободн\p{L}*\s+с|заселени\p{L}*\s+с|заезд\s+с|available\s+from|move[- ]?in\s+from)\s*[:\-]?\s*((?:\d{1,2}[./-]\d{1,2}(?:[./-]\d{2,4})?)|(?:\d{1,2}\s+[\p{L}]{3,12}(?:\s+\d{4})?))/iu);
48
+ return match?.[1]?.trim() || null;
49
+ }
50
+
51
+ function parseUtilitiesAmount(text) {
52
+ const match = text.match(/(?:коммунальн\p{L}*|коммуналк\p{L}*|ком\.?\s*услуг\p{L}*|utilities?|bills?)\D{0,24}(?:около|примерно|~|≈)?\s*(\d{2,8}(?:[.,]\d{1,2})?)\s*(₴|грн|uah|\$|usd|€|eur|сум|uzs|тг|kzt|lei|ron|руб|rub)?/iu);
53
+ if (!match) return null;
54
+ const amount = Number(match[1].replace(',', '.'));
55
+ if (!Number.isFinite(amount) || amount <= 0) return null;
56
+ const raw = (match[2] || '').toLocaleLowerCase();
57
+ const currency = /₴|грн|uah/u.test(raw) ? 'UAH'
58
+ : /\$|usd/u.test(raw) ? 'USD'
59
+ : /€|eur/u.test(raw) ? 'EUR'
60
+ : /сум|uzs/u.test(raw) ? 'UZS'
61
+ : /тг|kzt/u.test(raw) ? 'KZT'
62
+ : /lei|ron/u.test(raw) ? 'RON'
63
+ : /руб|rub/u.test(raw) ? 'RUB' : null;
64
+ return deepFreeze({ amount, currency, approximate: /около|примерно|~|≈/iu.test(match[0]) });
65
+ }
66
+
67
+ function parseCommunalSeparated(text, country) {
68
+ if (/(коммунал\p{L}*(?:\s+услуг\p{L}*)?\s*(?:отдельно|сверху|плюс|оплачива\p{L}*\s*отдельно)|свет\s*вода\s*газ\s*отдельно|kommunal\p{L}*\s*(?:alohida|ustiga)|utilities?\s*(?:separate|extra|not included))/iu.test(text)) return true;
69
+ if (/(коммунал\p{L}*(?:\s+услуг\p{L}*)?\s*(?:включ|входит|в\s*стоимост)|вс[её]\s*включ|all\s*inclusive|kommunal\p{L}*\s*(?:kiritilgan|ichida)|utilities?\s*included)/iu.test(text)) return false;
70
+ return String(country || '').toUpperCase() === 'UA' ? true : null;
71
+ }
72
+
73
+ function parseDepositRequired(text) {
74
+ if (/(?:без\s+(?:залога|депозита)|залог\s+не\s+нужен|депозит\s+не\s+нужен|no\s+deposit)/iu.test(text)) return false;
75
+ if (/(?:^|[^\p{L}\p{N}_])(?:залог|депозит|deposit|depozit|garantie|garanție|кепіл)(?=$|[^\p{L}\p{N}_])/iu.test(text)) return true;
76
+ return null;
77
+ }
78
+
79
+ export function parseHousingListingFields(value, { country = '' } = {}) {
80
+ const text = normalizeUnicode(value ?? '');
81
+ const context = parseHousingContext(text);
82
+ const features = parseHousingFeatures(text);
83
+ if (!text) return deepFreeze({});
84
+
85
+ const gas = bool(text,
86
+ /(?:^|[^\p{L}\p{N}_])(?:газ|gaz|gas)(?=$|[^\p{L}\p{N}_])|метан|aragaz|gaz\s+ta['’]?min/iu,
87
+ /без\s+газа|нет\s+газа|no\s+gas|gaz\s*yo['’]?q/iu,
88
+ );
89
+ const elevator = bool(text, /лифт|elevator|\blift\b/iu, /без\s*лифт|no\s*elevator|lift\s*yo['’]?q/iu);
90
+ const furnished = context.furniture === 'none' ? false : context.furniture ? true : null;
91
+ const childrenAllowed = context.tenantPolicies.children === 'allowed' ? true
92
+ : context.tenantPolicies.children === 'notAllowed' ? false : null;
93
+ const smokingAllowed = context.tenantPolicies.smoking === 'allowed' ? true
94
+ : context.tenantPolicies.smoking === 'notAllowed' ? false : null;
95
+
96
+ const firstRent = bool(text,
97
+ /первая\s+(?:сдача|аренда)|впервые\s+(?:сда[её]тся|сдается|в\s+аренду)|first\s+(?:rental|letting)|never\s+rented/iu,
98
+ );
99
+
100
+ return deepFreeze({
101
+ bedrooms: parseBedrooms(text),
102
+ bathrooms: parseBathrooms(text),
103
+ buildingYear: parseBuildingYear(text),
104
+ balcony: bool(text, /балкон|лоджи|balkon|ayvon|balcon|loggia|balcony/iu),
105
+ terrace: bool(text, /террас|terrace|teras[ăa]?/iu),
106
+ privateYard: bool(text, /личн\p{L}*\s+двор|сво[йеё]\s+(?:закрыт\p{L}*\s+)?двор|собственн\p{L}*\s+двор|приватн\p{L}*\s+двір|власн\p{L}*\s+двір|private\s+(?:courtyard|yard)|curte\s+(?:proprie|privat[ăa])|shaxsiy\s+hovli/iu),
107
+ courtyard: features.courtyard,
108
+ gazebo: features.gazebo,
109
+ dishwasher: bool(text, /посудомоечн\p{L}*|посудомойк\p{L}*|dishwasher|mașin[ăa]\s+de\s+spălat\s+vase/iu),
110
+ airConditioner: bool(text, /кондицион|сплит[- ]?систем|konditsioner|klimat|air\s*con|aer\s+condi[țt]ionat/iu),
111
+ gas,
112
+ newBuilding: bool(text, /новостро|новобуд|новый\s+дом|new\s*build|newly\s*built|yangi\s+(?:bino|qurilgan|uy)|bloc\s+nou/iu),
113
+ communalSeparated: parseCommunalSeparated(text, country),
114
+ parking: bool(text, /паркинг|парков|машино[- ]?мест|parking|avtoturargoh|mashina\s*joyi/iu),
115
+ elevator,
116
+ heating: bool(text, /отоплени|heating|otoplenie|isitish|markaziy\s*issiq/iu),
117
+ hotWater: bool(text, /горяч\p{L}*\s*вод|hot\s*water|issiq\s*suv/iu),
118
+ internet: features.internet,
119
+ petsAllowed: features.petsAllowed,
120
+ childrenAllowed,
121
+ smokingAllowed,
122
+ negotiable: context.priceModifiers.includes('fixed') ? false
123
+ : context.priceModifiers.includes('negotiable') ? true : null,
124
+ furnished,
125
+ depositRequired: parseDepositRequired(text),
126
+ firstRent,
127
+ minRentTerm: parseMinRentTerm(text),
128
+ availableFrom: parseAvailableFrom(text),
129
+ utilitiesAmount: parseUtilitiesAmount(text),
130
+ });
131
+ }
@@ -0,0 +1,7 @@
1
+ export type HousingPriceParseResult = Readonly<{
2
+ price: number | null;
3
+ currency: string;
4
+ }>;
5
+
6
+ export function parseHousingPrice(value: unknown, fallbackCurrency?: string): HousingPriceParseResult;
7
+ export const parsePriceFromText: typeof parseHousingPrice;