@whiteslove/parsing-lexicon 0.5.8 → 0.5.11
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 +3 -2
- package/src/hiring-context.js +8 -1
- package/src/hiring-source-semantics.js +7 -4
- package/src/hiring-work-semantics.d.ts +2 -0
- package/src/hiring-work-semantics.js +18 -0
- package/src/index.js +2 -1
- package/src/kg-location-extensions.js +25 -0
- package/src/locations-runtime.js +52 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whiteslove/parsing-lexicon",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.11",
|
|
4
4
|
"description": "Shared deterministic multilingual parsing lexicon for WhitesLove housing and hiring services",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"./central-asia": "./src/central-asia.js",
|
|
39
39
|
"./ukraine": "./src/ukraine.js",
|
|
40
40
|
"./location-merge": "./src/location-merge.js",
|
|
41
|
-
"./locations": "./src/locations.js",
|
|
41
|
+
"./locations": "./src/locations-runtime.js",
|
|
42
|
+
"./kg-location-extensions": "./src/kg-location-extensions.js",
|
|
42
43
|
"./kz-location-extensions": "./src/kz-location-extensions.js",
|
|
43
44
|
"./uz-location-extensions": "./src/uz-location-extensions.js",
|
|
44
45
|
"./central-asia-locations": "./src/central-asia-locations.js",
|
package/src/hiring-context.js
CHANGED
|
@@ -207,8 +207,15 @@ const SECTION_PATTERNS = Object.freeze(Object.fromEntries(
|
|
|
207
207
|
Object.entries(HIRING_SECTION_MARKERS).map(([key, entry]) => [key, sectionPattern(entry)]),
|
|
208
208
|
));
|
|
209
209
|
|
|
210
|
+
const INLINE_SECTION_LABELS = Object.values(HIRING_SECTION_MARKERS)
|
|
211
|
+
.flatMap((entry) => aliasesOf(entry))
|
|
212
|
+
.sort((a, b) => b.length - a.length)
|
|
213
|
+
.map((value) => escapeRegex(value).replace(/\s+/g, '\\s+'))
|
|
214
|
+
.join('|');
|
|
215
|
+
const INLINE_SECTION_MARKER_RE = new RegExp(`([^\\p{L}\\p{N}\\s])(?=(?:${INLINE_SECTION_LABELS})\\s*[::—-])`, 'giu');
|
|
216
|
+
|
|
210
217
|
export function splitHiringSections(value) {
|
|
211
|
-
const text = String(value || '').replace(/\r/g, '');
|
|
218
|
+
const text = String(value || '').replace(/\r/g, '').replace(INLINE_SECTION_MARKER_RE, '\n$1');
|
|
212
219
|
const out = {};
|
|
213
220
|
let current = null;
|
|
214
221
|
for (const rawLine of text.split('\n')) {
|
|
@@ -21,12 +21,15 @@ const FIELD_EXTRA_ALIASES = Object.freeze({
|
|
|
21
21
|
}),
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
const APPLICATION_MODAL_RE = /(?:отправить\s
|
|
25
|
-
const
|
|
24
|
+
const APPLICATION_MODAL_RE = /(?:отправить\s*резюме\s*)+(?:\[telegram\]\s*)?пол\s*мужской\s*женский\s*образование[\s\S]{0,500}?выберите\s*вакансию[\s\S]*/iu;
|
|
25
|
+
const LEAKED_STYLESHEET_START_RE = /:root\s*\{\s*--[\w-]+\s*:/iu;
|
|
26
|
+
const LEAKED_CSS_RULE_RE = /\s*(?:[.#][\w\\-]+|:root)(?:[^{}\n]{0,300})\{\s*(?:(?:--)?[\w-]+\s*:\s*[^;{}]+;?\s*)+\}\s*/giu;
|
|
26
27
|
|
|
27
|
-
/** Remove source-page controls and stylesheet fragments copied into
|
|
28
|
+
/** Remove source-page controls and stylesheet fragments copied into hiring text. */
|
|
28
29
|
export function cleanHiringSourceText(value) {
|
|
29
|
-
|
|
30
|
+
const text = String(value || '');
|
|
31
|
+
const stylesheetStart = text.search(LEAKED_STYLESHEET_START_RE);
|
|
32
|
+
return (stylesheetStart >= 0 ? text.slice(0, stylesheetStart) : text)
|
|
30
33
|
.replace(APPLICATION_MODAL_RE, '')
|
|
31
34
|
.replace(LEAKED_CSS_RULE_RE, ' ')
|
|
32
35
|
.replace(/[ \t]+(?=\n)/g, '')
|
|
@@ -26,9 +26,11 @@ export type HiringWorkSchedule =
|
|
|
26
26
|
|
|
27
27
|
export type HiringProbationKind = 'probation' | 'noProbation' | 'paidProbation' | 'unpaidProbation';
|
|
28
28
|
export type HiringExperienceRequirement = 'noExperience' | 'experienceRequired';
|
|
29
|
+
export type HiringWorkTimeRange = Readonly<{ start: string; end: string }>;
|
|
29
30
|
|
|
30
31
|
export function detectEmploymentTypes(value: unknown): readonly HiringEmploymentType[];
|
|
31
32
|
export function detectWorkModes(value: unknown): readonly HiringWorkMode[];
|
|
32
33
|
export function detectWorkSchedules(value: unknown): readonly HiringWorkSchedule[];
|
|
34
|
+
export function extractWorkTimeRanges(value: unknown): readonly HiringWorkTimeRange[];
|
|
33
35
|
export function detectProbation(value: unknown): HiringProbationKind | null;
|
|
34
36
|
export function detectExperienceRequirement(value: unknown): HiringExperienceRequirement | null;
|
|
@@ -66,6 +66,24 @@ export function detectWorkSchedules(value) {
|
|
|
66
66
|
return Object.freeze(collectCanonical(String(value || ''), [...SCHEDULE_TERMS, ...WORK_SCHEDULE_EXTENSIONS]));
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
const WORK_TIME_RANGE_RE = /(?:^|[^\p{L}\p{N}])(?:с|from|de\s+la)?\s*([01]?\d|2[0-3])[:.]([0-5]\d)\s*(?:[-–—]|до|to|până\s+la|pana\s+la|gacha)\s*([01]?\d|2[0-3])[:.]([0-5]\d)(?!\d)/giu;
|
|
70
|
+
|
|
71
|
+
/** Extract explicit working-time intervals without confusing them with contact hours. */
|
|
72
|
+
export function extractWorkTimeRanges(value) {
|
|
73
|
+
const text = String(value || '');
|
|
74
|
+
const ranges = [];
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
for (const match of text.matchAll(WORK_TIME_RANGE_RE)) {
|
|
77
|
+
const start = `${match[1].padStart(2, '0')}:${match[2]}`;
|
|
78
|
+
const end = `${match[3].padStart(2, '0')}:${match[4]}`;
|
|
79
|
+
const key = `${start}-${end}`;
|
|
80
|
+
if (seen.has(key)) continue;
|
|
81
|
+
seen.add(key);
|
|
82
|
+
ranges.push(Object.freeze({ start, end }));
|
|
83
|
+
}
|
|
84
|
+
return Object.freeze(ranges);
|
|
85
|
+
}
|
|
86
|
+
|
|
69
87
|
export function detectProbation(value) {
|
|
70
88
|
const text = String(value || '');
|
|
71
89
|
for (const entry of PROBATION_MATCH_ORDER) {
|
package/src/index.js
CHANGED
|
@@ -18,7 +18,8 @@ export * from './geography-detection.js';
|
|
|
18
18
|
export * from './central-asia.js';
|
|
19
19
|
export * from './ukraine.js';
|
|
20
20
|
export * from './location-merge.js';
|
|
21
|
-
export * from './locations.js';
|
|
21
|
+
export * from './locations-runtime.js';
|
|
22
|
+
export * from './kg-location-extensions.js';
|
|
22
23
|
export * from './kz-location-extensions.js';
|
|
23
24
|
export * from './uz-location-extensions.js';
|
|
24
25
|
export * from './central-asia-locations.js';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { aliasesToRegex } from './normalization.js';
|
|
2
|
+
|
|
3
|
+
const district = (name, aliases = []) => {
|
|
4
|
+
const all = Object.freeze([...new Set([name, ...aliases].filter(Boolean))]);
|
|
5
|
+
return Object.freeze({
|
|
6
|
+
canonical: name,
|
|
7
|
+
name,
|
|
8
|
+
type: 'district',
|
|
9
|
+
entityType: 'district',
|
|
10
|
+
country: 'KG',
|
|
11
|
+
aliases: all,
|
|
12
|
+
re: aliasesToRegex(all),
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const KG_LOCATION_EXTENSIONS = Object.freeze({
|
|
17
|
+
Bishkek: Object.freeze({
|
|
18
|
+
districts: Object.freeze([
|
|
19
|
+
district('Pervomaisky', ['Первомайский', 'Первомайский район', 'Биринчи Май', 'Биринчи Май району', 'Birinchi May', 'Birinchi May district']),
|
|
20
|
+
district('Leninsky', ['Ленинский', 'Ленинский район', 'Ленин району', 'Lenin district']),
|
|
21
|
+
district('Oktyabrsky', ['Октябрьский', 'Октябрьский район', 'Октябрь району', 'Oktyabr district']),
|
|
22
|
+
district('Sverdlovsky', ['Свердловский', 'Свердловский район', 'Свердлов району', 'Sverdlov district']),
|
|
23
|
+
]),
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LOCATION_DICTIONARIES as BASE_LOCATION_DICTIONARIES,
|
|
3
|
+
UA_REGION_ENTRIES,
|
|
4
|
+
UA_SECONDARY_CITIES,
|
|
5
|
+
matchUkraineRegion,
|
|
6
|
+
matchUkraineSecondaryCity,
|
|
7
|
+
} from './locations.js';
|
|
8
|
+
import { LOCATION_LIST_KEYS, mergeLocationCountries } from './location-merge.js';
|
|
9
|
+
import { KG_LOCATION_EXTENSIONS } from './kg-location-extensions.js';
|
|
10
|
+
|
|
11
|
+
export { UA_REGION_ENTRIES, UA_SECONDARY_CITIES, matchUkraineRegion, matchUkraineSecondaryCity };
|
|
12
|
+
|
|
13
|
+
export const LOCATION_DICTIONARIES = Object.freeze({
|
|
14
|
+
...BASE_LOCATION_DICTIONARIES,
|
|
15
|
+
KG: mergeLocationCountries(
|
|
16
|
+
BASE_LOCATION_DICTIONARIES.KG || {},
|
|
17
|
+
KG_LOCATION_EXTENSIONS,
|
|
18
|
+
),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export function dictionaryFor(countryCode, city) {
|
|
22
|
+
return LOCATION_DICTIONARIES[countryCode]?.[city] || null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function locationCities(countryCode) {
|
|
26
|
+
return LOCATION_DICTIONARIES[countryCode] || Object.freeze({});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function matchDictionaryLocation(text, countryCode, city = null) {
|
|
30
|
+
const country = locationCities(countryCode);
|
|
31
|
+
const cities = city && country[city] ? [[city, country[city]]] : Object.entries(country);
|
|
32
|
+
const value = String(text || '');
|
|
33
|
+
let best = null;
|
|
34
|
+
|
|
35
|
+
for (const [cityName, data] of cities) {
|
|
36
|
+
for (const type of LOCATION_LIST_KEYS) {
|
|
37
|
+
for (const entry of data[type] || []) {
|
|
38
|
+
const match = entry?.re?.exec(value);
|
|
39
|
+
if (!match) continue;
|
|
40
|
+
const start = match.index;
|
|
41
|
+
const end = start + match[0].length;
|
|
42
|
+
const containsBest = best && start <= best.start && end >= best.end && end - start > best.end - best.start;
|
|
43
|
+
if (best && !containsBest) continue;
|
|
44
|
+
best = { city: cityName, type, name: entry.name, aliases: entry.aliases, start, end };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!best) return null;
|
|
50
|
+
const { start: _start, end: _end, ...result } = best;
|
|
51
|
+
return result;
|
|
52
|
+
}
|