@whiteslove/parsing-lexicon 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +137 -137
  3. package/index.d.ts +374 -374
  4. package/package.json +77 -75
  5. package/src/central-asia-locations.js +211 -211
  6. package/src/central-asia.js +233 -233
  7. package/src/contact.d.ts +44 -44
  8. package/src/contact.js +159 -159
  9. package/src/countries.js +175 -175
  10. package/src/country-context.d.ts +10 -10
  11. package/src/country-context.js +22 -22
  12. package/src/currency.d.ts +14 -14
  13. package/src/currency.js +66 -66
  14. package/src/geo.js +169 -169
  15. package/src/geography-central-asia.js +42 -42
  16. package/src/geography-detection.d.ts +14 -14
  17. package/src/geography-detection.js +64 -64
  18. package/src/geography-display.d.ts +13 -13
  19. package/src/geography-display.js +113 -113
  20. package/src/geography.js +210 -210
  21. package/src/hiring-advanced.js +147 -147
  22. package/src/hiring-candidate-fields.d.ts +9 -9
  23. package/src/hiring-candidate-fields.js +92 -92
  24. package/src/hiring-context.d.ts +64 -64
  25. package/src/hiring-context.js +412 -412
  26. package/src/hiring-language-extensions.d.ts +19 -19
  27. package/src/hiring-language-extensions.js +139 -139
  28. package/src/hiring-languages.js +135 -135
  29. package/src/hiring-location-fields.d.ts +3 -3
  30. package/src/hiring-location-fields.js +53 -53
  31. package/src/hiring-professions-ro.js +191 -191
  32. package/src/hiring-professions.js +437 -437
  33. package/src/hiring-requirements.d.ts +16 -16
  34. package/src/hiring-requirements.js +170 -170
  35. package/src/hiring-salary-context.d.ts +27 -27
  36. package/src/hiring-salary-context.js +76 -76
  37. package/src/hiring-semantics.d.ts +29 -29
  38. package/src/hiring-semantics.js +218 -218
  39. package/src/hiring-skills.d.ts +21 -21
  40. package/src/hiring-skills.js +315 -315
  41. package/src/hiring-source-aliases.d.ts +32 -32
  42. package/src/hiring-source-aliases.js +185 -185
  43. package/src/hiring-source-semantics.d.ts +31 -31
  44. package/src/hiring-source-semantics.js +293 -293
  45. package/src/hiring-temporal.d.ts +15 -15
  46. package/src/hiring-temporal.js +67 -67
  47. package/src/hiring-vacancy-fields.d.ts +5 -5
  48. package/src/hiring-vacancy-fields.js +23 -23
  49. package/src/hiring-work-semantics.d.ts +34 -34
  50. package/src/hiring-work-semantics.js +72 -72
  51. package/src/hiring.js +180 -180
  52. package/src/housing-address.d.ts +18 -18
  53. package/src/housing-address.js +217 -210
  54. package/src/housing-commercial.d.ts +1 -0
  55. package/src/housing-commercial.js +9 -0
  56. package/src/housing-context.d.ts +36 -36
  57. package/src/housing-context.js +183 -183
  58. package/src/housing-features.js +30 -30
  59. package/src/housing-intent.d.ts +9 -9
  60. package/src/housing-intent.js +132 -132
  61. package/src/housing-listing-fields.js +131 -131
  62. package/src/housing-money.d.ts +7 -7
  63. package/src/housing-money.js +102 -102
  64. package/src/housing-source-aliases.d.ts +8 -8
  65. package/src/housing-source-aliases.js +51 -51
  66. package/src/housing-structured.d.ts +55 -55
  67. package/src/housing-structured.js +219 -219
  68. package/src/housing.js +200 -200
  69. package/src/index.js +45 -44
  70. package/src/kz-location-extensions.js +307 -307
  71. package/src/landmarks.js +21 -21
  72. package/src/lexicon-core.js +58 -58
  73. package/src/location-merge.js +106 -106
  74. package/src/locations.js +456 -456
  75. package/src/money-core.d.ts +10 -10
  76. package/src/money-core.js +81 -81
  77. package/src/money-lexicon.d.ts +5 -5
  78. package/src/money-lexicon.js +79 -79
  79. package/src/money.js +154 -154
  80. package/src/normalization.js +384 -384
  81. package/src/odesa-metropolitan.js +144 -144
  82. package/src/romania-geography.js +47 -47
  83. package/src/tashkent-colloquial.js +80 -72
  84. package/src/tashkent-housing-geography.d.ts +38 -38
  85. package/src/tashkent-housing-geography.js +211 -211
  86. package/src/tashkent-pois.js +215 -215
  87. package/src/tashkent-residential-complexes.js +272 -272
  88. package/src/ua-location-extensions-major.js +118 -118
  89. package/src/ua-location-extensions-metro.js +9 -9
  90. package/src/ua-location-extensions-regional.js +246 -246
  91. package/src/ua-secondary-cities.d.ts +2 -2
  92. package/src/ua-secondary-cities.js +57 -57
  93. package/src/ukraine.js +75 -75
  94. package/src/uz-location-extensions.js +259 -259
@@ -1,210 +1,217 @@
1
- const PHONE_RUN_RE = /\+?\d[\d\s().-]{7,}\d/gu;
2
- const ADDRESS_LABEL_RE = /(?:адрес|адреса|адресація|адресация|manzil|address|adresă|adresa)\s*[:=\-–—]\s*/iu;
3
- const PREFIX_STREET_MARKER = String.raw`(?:(?:ул(?:ица)?|вул(?:иця)?|просп(?:ект)?|пр-т|переул(?:ок)?|пров(?:улок)?|проезд|наб(?:ережная)?|шоссе|str(?:ada)?|street|st|avenue|ave|road|rd|көше)\.?)`;
4
- const POSTFIX_STREET_MARKER = String.raw`(?:ko['’ʼ\u02bc]?cha(?:si)?|кўча(?:си)?|коча(?:си)?|kocha(?:si)?)`;
5
- const POSTFIX_STREET_TYPE = String.raw`(?:вулиця|улица|провулок|переулок|проспект|бульвар|набережна|набережная|шосе|шоссе|площа|площадь|узвіз|спуск|алея|аллея|дорога|тупик)`;
6
- const HOUSE_MARKER = String.raw`(?:дом|д\.|будинок|буд\.|house|h\.|uy|уй|nr\.?|no\.?|№)`;
7
- const BUILDING_MARKER = String.raw`(?:корп(?:ус)?\.?|к\.|строен(?:ие)?|стр\.|будова|секц(?:ия|ія)?|bloc|corp|building|bldg\.?|korpus)`;
8
- const NUMBER_TOKEN = String.raw`\d{1,5}(?:[-\/]?[\p{L}])?(?:[\/-]\d{1,4}(?:[-\/]?[\p{L}])?)?`;
9
- const STREET_WORD = String.raw`[\p{L}'’.-]{2,48}`;
10
-
11
- function clean(value) {
12
- return String(value ?? '')
13
- .normalize('NFKC')
14
- .replace(PHONE_RUN_RE, (segment) => segment.replace(/\D/gu, '').length >= 9 ? ' ' : segment)
15
- .replace(/[\t\f\v]+/gu, ' ')
16
- .replace(/\s{2,}/gu, ' ')
17
- .replace(/^[\s,;:.\-–—]+|[\s,;:.\-–—]+$/gu, '')
18
- .trim();
19
- }
20
-
21
- function escapeRegExp(value) {
22
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
23
- }
24
-
25
- function compactStreet(value) {
26
- return clean(value)
27
- .replace(new RegExp(`^${PREFIX_STREET_MARKER}\\s*`, 'iu'), '')
28
- .replace(new RegExp(`\\s+${POSTFIX_STREET_MARKER}$`, 'iu'), '')
29
- .replace(/[\s,;:.\-–—]+$/gu, '')
30
- .trim() || null;
31
- }
32
-
33
- function normalizeNumber(value) {
34
- const result = clean(value).replace(/\s+/gu, '');
35
- return result || null;
36
- }
37
-
38
- function result(address, street = null, houseNumber = null, building = null, confidence = 0) {
39
- return Object.freeze({
40
- address: clean(address) || null,
41
- street: compactStreet(street),
42
- houseNumber: normalizeNumber(houseNumber),
43
- building: normalizeNumber(building),
44
- confidence,
45
- });
46
- }
47
-
48
- function splitAddressTail(raw) {
49
- const text = clean(raw);
50
- if (!text) return null;
51
-
52
- const buildingRe = new RegExp(`(?:\\s*[,;]?\\s*${BUILDING_MARKER}\\s*(${NUMBER_TOKEN}))\\s*$`, 'iu');
53
- const buildingMatch = text.match(buildingRe);
54
- const building = buildingMatch?.[1] || null;
55
- const withoutBuilding = buildingMatch ? clean(text.slice(0, buildingMatch.index)) : text;
56
-
57
- const markedHouse = withoutBuilding.match(new RegExp(`^(.*?)\\s*[,;]?\\s*${HOUSE_MARKER}\\s*(${NUMBER_TOKEN})\\s*$`, 'iu'));
58
- if (markedHouse && /\p{L}{2,}/u.test(markedHouse[1])) {
59
- return { street: markedHouse[1], houseNumber: markedHouse[2], building };
60
- }
61
-
62
- const trailingHouse = withoutBuilding.match(new RegExp(`^(.*?\\p{L}.*?)\\s+(${NUMBER_TOKEN})\\s*$`, 'iu'));
63
- if (trailingHouse && /\p{L}{2,}/u.test(trailingHouse[1])) {
64
- return { street: trailingHouse[1], houseNumber: trailingHouse[2], building };
65
- }
66
-
67
- return /\p{L}{2,}/u.test(withoutBuilding)
68
- ? { street: withoutBuilding, houseNumber: null, building }
69
- : null;
70
- }
71
-
72
- function postfixTypedStreetAddress(line) {
73
- const suffix = line.match(new RegExp(
74
- `(?:^|[^\\p{L}\\p{N}])((?:${STREET_WORD}\\s+){0,4}${STREET_WORD}\\s+${POSTFIX_STREET_TYPE})` +
75
- `\\s*[,;]?\\s*(${NUMBER_TOKEN})` +
76
- `(?:\\s*[,;]?\\s*${BUILDING_MARKER}\\s*(${NUMBER_TOKEN}))?` +
77
- `(?=$|[^\\p{L}\\p{N}])`,
78
- 'iu',
79
- ));
80
- if (!suffix) return null;
81
-
82
- const street = suffix[1];
83
- const houseNumber = suffix[2];
84
- const building = suffix[3] || null;
85
- const address = composeHousingAddress({ street, houseNumber, building });
86
- return result(address, street, houseNumber, building, 1);
87
- }
88
-
89
- function prefixTypedStreetAddress(line) {
90
- const prefix = line.match(new RegExp(
91
- `(?:^|[\\s,;])${PREFIX_STREET_MARKER}\\s+` +
92
- `((?:${STREET_WORD}\\s+){0,4}${STREET_WORD})` +
93
- `\\s*[,;]?\\s*(?:${HOUSE_MARKER}\\s*)?(${NUMBER_TOKEN})` +
94
- `(?:\\s*[,;]?\\s*${BUILDING_MARKER}\\s*(${NUMBER_TOKEN}))?` +
95
- `(?=$|[^\\p{L}\\p{N}])`,
96
- 'iu',
97
- ));
98
- if (!prefix) return null;
99
-
100
- const street = prefix[1];
101
- const houseNumber = prefix[2];
102
- const building = prefix[3] || null;
103
- const address = composeHousingAddress({ street, houseNumber, building });
104
- return result(address, street, houseNumber, building, 1);
105
- }
106
-
107
- function explicitStreetAddress(text) {
108
- const lines = text
109
- .split(/[\r\n|]/u)
110
- .map((part) => clean(part).slice(0, 160))
111
- .filter(Boolean)
112
- .slice(0, 12);
113
-
114
- for (const line of lines) {
115
- const postfixTyped = postfixTypedStreetAddress(line);
116
- if (postfixTyped) return postfixTyped;
117
-
118
- const prefixTyped = prefixTypedStreetAddress(line);
119
- if (prefixTyped) return prefixTyped;
120
-
121
- const prefix = line.match(new RegExp(`(?:^|[\\s,;])(${PREFIX_STREET_MARKER})\\s+(.+)$`, 'iu'));
122
- if (prefix) {
123
- const tail = splitAddressTail(prefix[2]);
124
- if (tail) return result(line, tail.street, tail.houseNumber, tail.building, tail.houseNumber ? 1 : 0.9);
125
- }
126
-
127
- const postfix = line.match(new RegExp(`^(.+?)\\s+(${POSTFIX_STREET_MARKER})(.*)$`, 'iu'));
128
- if (postfix) {
129
- const tailText = clean(`${postfix[1]} ${postfix[3]}`);
130
- const tail = splitAddressTail(tailText);
131
- if (tail) return result(line, tail.street, tail.houseNumber, tail.building, tail.houseNumber ? 1 : 0.9);
132
- }
133
- }
134
-
135
- return null;
136
- }
137
-
138
- function knownStreetAddress(text, knownStreet) {
139
- const street = compactStreet(knownStreet);
140
- if (!street) return null;
141
- const streetPattern = street.split(/\s+/u).map(escapeRegExp).join('\\s+');
142
- const re = new RegExp(
143
- `(?:^|[^\\p{L}\\p{N}])(?:${PREFIX_STREET_MARKER}\\s+)?(${streetPattern})(?:\\s+${POSTFIX_STREET_MARKER})?` +
144
- `(?:\\s*[,;]?\\s*(?:${HOUSE_MARKER})?\\s*(${NUMBER_TOKEN}))?` +
145
- `(?:\\s*[,;]?\\s*${BUILDING_MARKER}\\s*(${NUMBER_TOKEN}))?` +
146
- `(?=$|[^\\p{L}\\p{N}])`,
147
- 'iu',
148
- );
149
- const match = text.match(re);
150
- if (!match) return null;
151
- const houseNumber = match[2] || null;
152
- const building = match[3] || null;
153
- const address = composeHousingAddress({ street, houseNumber, building });
154
- return result(address, street, houseNumber, building, houseNumber ? 0.98 : 0.9);
155
- }
156
-
157
- function labelledAddress(text) {
158
- const label = text.match(ADDRESS_LABEL_RE);
159
- if (!label) return null;
160
- const start = (label.index ?? 0) + label[0].length;
161
- const line = clean(text.slice(start).split(/[\r\n|]/u, 1)[0]).slice(0, 140);
162
- if (!line) return null;
163
- return parseHousingAddress(line, { allowBare: true });
164
- }
165
-
166
- function bareAddress(text) {
167
- const tail = splitAddressTail(text);
168
- if (!tail) return null;
169
- const confidence = tail.houseNumber ? 0.85 : 0.55;
170
- return result(text, tail.street, tail.houseNumber, tail.building, confidence);
171
- }
172
-
173
- /**
174
- * Parse only textual address structure. This module intentionally contains no
175
- * coordinates and performs no geocoding.
176
- *
177
- * `knownStreet` may be supplied when a location dictionary has already
178
- * canonicalized the street. In that mode the parser only accepts a house/building
179
- * number immediately adjacent to that exact street mention.
180
- *
181
- * `allowBare` should be used only when the input is already known to be an
182
- * address field (for example a source-provided address), not on arbitrary post
183
- * prose where prices and phone numbers may look like house numbers.
184
- */
185
- export function parseHousingAddress(value, options = {}) {
186
- const text = clean(value);
187
- if (!text) return result(null);
188
-
189
- const labelled = labelledAddress(text);
190
- if (labelled) return labelled;
191
-
192
- if (options.knownStreet) {
193
- const known = knownStreetAddress(text, options.knownStreet);
194
- if (known) return known;
195
- }
196
-
197
- const explicit = explicitStreetAddress(text);
198
- if (explicit) return explicit;
199
-
200
- if (options.allowBare === true) return bareAddress(text) || result(text);
201
- return result(null);
202
- }
203
-
204
- export function composeHousingAddress(parts = {}) {
205
- const street = compactStreet(parts.street);
206
- const houseNumber = normalizeNumber(parts.houseNumber);
207
- const building = normalizeNumber(parts.building);
208
- if (!street) return null;
209
- return [street, houseNumber, building ? `корп. ${building}` : null].filter(Boolean).join(' ');
210
- }
1
+ const PHONE_RUN_RE = /\+?\d[\d\s().-]{7,}\d/gu;
2
+ const ADDRESS_LABEL_RE = /(?:адрес|адреса|адресація|адресация|manzil|address|adresă|adresa)\s*[:=\-–—]\s*/iu;
3
+ const PREFIX_STREET_MARKER = String.raw`(?:(?:ул(?:ица)?|вул(?:иця)?|просп(?:ект)?|пр-т|переул(?:ок)?|пров(?:улок)?|проезд|наб(?:ережная)?|шоссе|str(?:ada)?|street|st|avenue|ave|road|rd|көше)\.?)`;
4
+ const POSTFIX_STREET_MARKER = String.raw`(?:ko['’ʼ\u02bc]?cha(?:si)?|кўча(?:си)?|коча(?:си)?|kocha(?:si)?)`;
5
+ const POSTFIX_STREET_TYPE = String.raw`(?:вулиця|улица|провулок|переулок|проспект|бульвар|набережна|набережная|шосе|шоссе|площа|площадь|узвіз|спуск|алея|аллея|дорога|тупик)`;
6
+ const HOUSE_MARKER = String.raw`(?:дом|д\.|будинок|буд\.|house|h\.|uy|уй|nr\.?|no\.?|№)`;
7
+ const BUILDING_MARKER = String.raw`(?:корп(?:ус)?\.?|к\.|строен(?:ие)?|стр\.|будова|секц(?:ия|ія)?|bloc|corp|building|bldg\.?|korpus)`;
8
+ const NUMBER_TOKEN = String.raw`\d{1,5}(?:[-\/]?[\p{L}])?(?:[\/-]\d{1,4}(?:[-\/]?[\p{L}])?)?`;
9
+ const STREET_WORD = String.raw`[\p{L}'’.-]{2,48}`;
10
+
11
+ function clean(value) {
12
+ return String(value ?? '')
13
+ .normalize('NFKC')
14
+ .replace(PHONE_RUN_RE, (segment) => segment.replace(/\D/gu, '').length >= 9 ? ' ' : segment)
15
+ .replace(/[\t\f\v]+/gu, ' ')
16
+ .replace(/\s{2,}/gu, ' ')
17
+ .replace(/^[\s,;:.\-–—]+|[\s,;:.\-–—]+$/gu, '')
18
+ .trim();
19
+ }
20
+
21
+ function escapeRegExp(value) {
22
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
23
+ }
24
+
25
+ function compactStreet(value) {
26
+ return clean(value)
27
+ .replace(new RegExp(`^${PREFIX_STREET_MARKER}\\s*`, 'iu'), '')
28
+ .replace(new RegExp(`\\s+${POSTFIX_STREET_MARKER}$`, 'iu'), '')
29
+ .replace(/[\s,;:.\-–—]+$/gu, '')
30
+ .trim() || null;
31
+ }
32
+
33
+ function normalizeNumber(value) {
34
+ const result = clean(value).replace(/\s+/gu, '');
35
+ return result || null;
36
+ }
37
+
38
+ function result(address, street = null, houseNumber = null, building = null, confidence = 0) {
39
+ return Object.freeze({
40
+ address: clean(address) || null,
41
+ street: compactStreet(street),
42
+ houseNumber: normalizeNumber(houseNumber),
43
+ building: normalizeNumber(building),
44
+ confidence,
45
+ });
46
+ }
47
+
48
+ function splitAddressTail(raw) {
49
+ const text = clean(raw);
50
+ if (!text) return null;
51
+
52
+ const buildingRe = new RegExp(`(?:\\s*[,;]?\\s*${BUILDING_MARKER}\\s*(${NUMBER_TOKEN}))\\s*$`, 'iu');
53
+ const buildingMatch = text.match(buildingRe);
54
+ const building = buildingMatch?.[1] || null;
55
+ const withoutBuilding = buildingMatch ? clean(text.slice(0, buildingMatch.index)) : text;
56
+
57
+ const markedHouse = withoutBuilding.match(new RegExp(`^(.*?)\\s*[,;]?\\s*${HOUSE_MARKER}\\s*(${NUMBER_TOKEN})\\s*$`, 'iu'));
58
+ if (markedHouse && /\p{L}{2,}/u.test(markedHouse[1])) {
59
+ return { street: markedHouse[1], houseNumber: markedHouse[2], building };
60
+ }
61
+
62
+ const trailingHouse = withoutBuilding.match(new RegExp(`^(.*?\\p{L}.*?)\\s+(${NUMBER_TOKEN})\\s*$`, 'iu'));
63
+ if (trailingHouse && /\p{L}{2,}/u.test(trailingHouse[1])) {
64
+ return { street: trailingHouse[1], houseNumber: trailingHouse[2], building };
65
+ }
66
+
67
+ return /\p{L}{2,}/u.test(withoutBuilding)
68
+ ? { street: withoutBuilding, houseNumber: null, building }
69
+ : null;
70
+ }
71
+
72
+ function postfixTypedStreetAddress(line) {
73
+ const suffix = line.match(new RegExp(
74
+ `(?:^|[^\\p{L}\\p{N}])((?:${STREET_WORD}\\s+){0,4}${STREET_WORD}\\s+${POSTFIX_STREET_TYPE})` +
75
+ `\\s*[,;]?\\s*(${NUMBER_TOKEN})` +
76
+ `(?:\\s*[,;]?\\s*${BUILDING_MARKER}\\s*(${NUMBER_TOKEN}))?` +
77
+ `(?=$|[^\\p{L}\\p{N}])`,
78
+ 'iu',
79
+ ));
80
+ if (!suffix) return null;
81
+
82
+ const street = suffix[1];
83
+ const houseNumber = suffix[2];
84
+ const building = suffix[3] || null;
85
+ const address = composeHousingAddress({ street, houseNumber, building });
86
+ return result(address, street, houseNumber, building, 1);
87
+ }
88
+
89
+ function prefixTypedStreetAddress(line) {
90
+ const prefix = line.match(new RegExp(
91
+ `(?:^|[\\s,;])${PREFIX_STREET_MARKER}\\s+` +
92
+ `((?:${STREET_WORD}\\s+){0,4}${STREET_WORD})` +
93
+ `\\s*[,;]?\\s*(?:${HOUSE_MARKER}\\s*)?(${NUMBER_TOKEN})` +
94
+ `(?:\\s*[,;]?\\s*${BUILDING_MARKER}\\s*(${NUMBER_TOKEN}))?` +
95
+ `(?=$|[^\\p{L}\\p{N}])`,
96
+ 'iu',
97
+ ));
98
+ if (!prefix) return null;
99
+
100
+ const street = prefix[1];
101
+ const houseNumber = prefix[2];
102
+ const building = prefix[3] || null;
103
+ const address = composeHousingAddress({ street, houseNumber, building });
104
+ return result(address, street, houseNumber, building, 1);
105
+ }
106
+
107
+ function explicitStreetAddress(text) {
108
+ const lines = text
109
+ .split(/[\r\n|]/u)
110
+ .map((part) => clean(part).slice(0, 160))
111
+ .filter(Boolean)
112
+ .slice(0, 12);
113
+
114
+ for (const line of lines) {
115
+ const postfixTyped = postfixTypedStreetAddress(line);
116
+ if (postfixTyped) return postfixTyped;
117
+
118
+ const prefixTyped = prefixTypedStreetAddress(line);
119
+ if (prefixTyped) return prefixTyped;
120
+
121
+ const boundedPrefix = line.match(new RegExp(
122
+ `(?:^|[\\s,;])${PREFIX_STREET_MARKER}\\s*((?:${STREET_WORD}\\s+){0,3}${STREET_WORD})(?=$|[,;])`,
123
+ 'iu',
124
+ ));
125
+ if (boundedPrefix) return result(boundedPrefix[0], boundedPrefix[1], null, null, 0.9);
126
+
127
+ const prefix = line.match(new RegExp(`(?:^|[\\s,;])(${PREFIX_STREET_MARKER})\\s+(.+)$`, 'iu'));
128
+ if (prefix) {
129
+ const tail = splitAddressTail(prefix[2]);
130
+ if (tail) return result(line, tail.street, tail.houseNumber, tail.building, tail.houseNumber ? 1 : 0.9);
131
+ }
132
+
133
+ const postfix = line.match(new RegExp(`^(.+?)\\s+(${POSTFIX_STREET_MARKER})(.*)$`, 'iu'));
134
+ if (postfix) {
135
+ const tailText = clean(`${postfix[1]} ${postfix[3]}`);
136
+ const tail = splitAddressTail(tailText);
137
+ if (tail) return result(line, tail.street, tail.houseNumber, tail.building, tail.houseNumber ? 1 : 0.9);
138
+ }
139
+ }
140
+
141
+ return null;
142
+ }
143
+
144
+ function knownStreetAddress(text, knownStreet) {
145
+ const street = compactStreet(knownStreet);
146
+ if (!street) return null;
147
+ const streetPattern = street.split(/\s+/u).map(escapeRegExp).join('\\s+');
148
+ const re = new RegExp(
149
+ `(?:^|[^\\p{L}\\p{N}])(?:${PREFIX_STREET_MARKER}\\s+)?(${streetPattern})(?:\\s+${POSTFIX_STREET_MARKER})?` +
150
+ `(?:\\s*[,;]?\\s*(?:${HOUSE_MARKER})?\\s*(${NUMBER_TOKEN}))?` +
151
+ `(?:\\s*[,;]?\\s*${BUILDING_MARKER}\\s*(${NUMBER_TOKEN}))?` +
152
+ `(?=$|[^\\p{L}\\p{N}])`,
153
+ 'iu',
154
+ );
155
+ const match = text.match(re);
156
+ if (!match) return null;
157
+ const houseNumber = match[2] || null;
158
+ const building = match[3] || null;
159
+ const address = composeHousingAddress({ street, houseNumber, building });
160
+ return result(address, street, houseNumber, building, houseNumber ? 0.98 : 0.9);
161
+ }
162
+
163
+ function labelledAddress(text) {
164
+ const label = text.match(ADDRESS_LABEL_RE);
165
+ if (!label) return null;
166
+ const start = (label.index ?? 0) + label[0].length;
167
+ const line = clean(text.slice(start).split(/[\r\n|]/u, 1)[0]).slice(0, 140);
168
+ if (!line) return null;
169
+ return parseHousingAddress(line, { allowBare: true });
170
+ }
171
+
172
+ function bareAddress(text) {
173
+ const tail = splitAddressTail(text);
174
+ if (!tail) return null;
175
+ if (/^(?:центр|centre|center)$/iu.test(compactStreet(tail.street) || '')) return null;
176
+ const confidence = tail.houseNumber ? 0.85 : 0.55;
177
+ return result(text, tail.street, tail.houseNumber, tail.building, confidence);
178
+ }
179
+
180
+ /**
181
+ * Parse only textual address structure. This module intentionally contains no
182
+ * coordinates and performs no geocoding.
183
+ *
184
+ * `knownStreet` may be supplied when a location dictionary has already
185
+ * canonicalized the street. In that mode the parser only accepts a house/building
186
+ * number immediately adjacent to that exact street mention.
187
+ *
188
+ * `allowBare` should be used only when the input is already known to be an
189
+ * address field (for example a source-provided address), not on arbitrary post
190
+ * prose where prices and phone numbers may look like house numbers.
191
+ */
192
+ export function parseHousingAddress(value, options = {}) {
193
+ const text = clean(value);
194
+ if (!text) return result(null);
195
+
196
+ const labelled = labelledAddress(text);
197
+ if (labelled) return labelled;
198
+
199
+ if (options.knownStreet) {
200
+ const known = knownStreetAddress(text, options.knownStreet);
201
+ if (known) return known;
202
+ }
203
+
204
+ const explicit = explicitStreetAddress(text);
205
+ if (explicit) return explicit;
206
+
207
+ if (options.allowBare === true) return bareAddress(text) || result(text);
208
+ return result(null);
209
+ }
210
+
211
+ export function composeHousingAddress(parts = {}) {
212
+ const street = compactStreet(parts.street);
213
+ const houseNumber = normalizeNumber(parts.houseNumber);
214
+ const building = normalizeNumber(parts.building);
215
+ if (!street) return null;
216
+ return [street, houseNumber, building ? `корп. ${building}` : null].filter(Boolean).join(' ');
217
+ }
@@ -0,0 +1 @@
1
+ export declare function looksCommercialHousing(value: unknown): boolean;
@@ -0,0 +1,9 @@
1
+ // Non-residential/commercial listing signals that should be excluded from
2
+ // residential housing feeds. Kept conservative so nearby amenities (for
3
+ // example "магазин рядом") do not classify an otherwise valid home as commercial.
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;
6
+
7
+ export function looksCommercialHousing(value) {
8
+ return value ? COMMERCIAL_HOUSING_RE.test(String(value)) : false;
9
+ }
@@ -1,36 +1,36 @@
1
- export type HousingPolicy = 'allowed' | 'notAllowed' | 'conditional' | null;
2
- export type HousingContextResult = Readonly<{
3
- condition: string | null;
4
- layouts: readonly string[];
5
- buildingType: string | null;
6
- buildingStatus: string | null;
7
- priceContext: string | null;
8
- priceModifiers: readonly string[];
9
- rentDuration: string | null;
10
- floorConstraints: readonly string[];
11
- furniture: string | null;
12
- tenantPolicies: Readonly<{ pets: HousingPolicy; children: HousingPolicy; smoking: HousingPolicy }>;
13
- documents: readonly string[];
14
- financing: readonly string[];
15
- locationRelations: readonly string[];
16
- availability: string | null;
17
- listingStatus: string | null;
18
- }>;
19
- export const PROPERTY_CONDITION_TERMS: readonly unknown[];
20
- export const LAYOUT_TERMS: readonly unknown[];
21
- export const BUILDING_TYPE_TERMS: readonly unknown[];
22
- export const BUILDING_STATUS_TERMS: readonly unknown[];
23
- export const PROPERTY_PRICE_CONTEXT: readonly unknown[];
24
- export const PRICE_MODIFIERS: readonly unknown[];
25
- export const RENT_DURATION_TERMS: readonly unknown[];
26
- export const FLOOR_CONSTRAINT_TERMS: readonly unknown[];
27
- export const FURNITURE_STATE_TERMS: readonly unknown[];
28
- export const PET_POLICY_TERMS: readonly unknown[];
29
- export const CHILD_POLICY_TERMS: readonly unknown[];
30
- export const SMOKING_POLICY_TERMS: readonly unknown[];
31
- export const DOCUMENT_STATUS_TERMS: readonly unknown[];
32
- export const FINANCING_TERMS: readonly unknown[];
33
- export const LOCATION_RELATIONS: readonly unknown[];
34
- export const AVAILABILITY_TERMS: readonly unknown[];
35
- export const LISTING_STATUS_TERMS: readonly unknown[];
36
- export function parseHousingContext(value: unknown): HousingContextResult;
1
+ export type HousingPolicy = 'allowed' | 'notAllowed' | 'conditional' | null;
2
+ export type HousingContextResult = Readonly<{
3
+ condition: string | null;
4
+ layouts: readonly string[];
5
+ buildingType: string | null;
6
+ buildingStatus: string | null;
7
+ priceContext: string | null;
8
+ priceModifiers: readonly string[];
9
+ rentDuration: string | null;
10
+ floorConstraints: readonly string[];
11
+ furniture: string | null;
12
+ tenantPolicies: Readonly<{ pets: HousingPolicy; children: HousingPolicy; smoking: HousingPolicy }>;
13
+ documents: readonly string[];
14
+ financing: readonly string[];
15
+ locationRelations: readonly string[];
16
+ availability: string | null;
17
+ listingStatus: string | null;
18
+ }>;
19
+ export const PROPERTY_CONDITION_TERMS: readonly unknown[];
20
+ export const LAYOUT_TERMS: readonly unknown[];
21
+ export const BUILDING_TYPE_TERMS: readonly unknown[];
22
+ export const BUILDING_STATUS_TERMS: readonly unknown[];
23
+ export const PROPERTY_PRICE_CONTEXT: readonly unknown[];
24
+ export const PRICE_MODIFIERS: readonly unknown[];
25
+ export const RENT_DURATION_TERMS: readonly unknown[];
26
+ export const FLOOR_CONSTRAINT_TERMS: readonly unknown[];
27
+ export const FURNITURE_STATE_TERMS: readonly unknown[];
28
+ export const PET_POLICY_TERMS: readonly unknown[];
29
+ export const CHILD_POLICY_TERMS: readonly unknown[];
30
+ export const SMOKING_POLICY_TERMS: readonly unknown[];
31
+ export const DOCUMENT_STATUS_TERMS: readonly unknown[];
32
+ export const FINANCING_TERMS: readonly unknown[];
33
+ export const LOCATION_RELATIONS: readonly unknown[];
34
+ export const AVAILABILITY_TERMS: readonly unknown[];
35
+ export const LISTING_STATUS_TERMS: readonly unknown[];
36
+ export function parseHousingContext(value: unknown): HousingContextResult;