@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.
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/index.d.ts +375 -0
- package/package.json +75 -0
- package/src/central-asia-locations.js +211 -0
- package/src/central-asia.js +233 -0
- package/src/contact.d.ts +44 -0
- package/src/contact.js +159 -0
- package/src/countries.js +175 -0
- package/src/country-context.d.ts +10 -0
- package/src/country-context.js +22 -0
- package/src/currency.d.ts +14 -0
- package/src/currency.js +66 -0
- package/src/geo.js +169 -0
- package/src/geography-central-asia.js +42 -0
- package/src/geography-detection.d.ts +14 -0
- package/src/geography-detection.js +64 -0
- package/src/geography-display.d.ts +13 -0
- package/src/geography-display.js +113 -0
- package/src/geography.js +210 -0
- package/src/hiring-advanced.js +147 -0
- package/src/hiring-candidate-fields.d.ts +9 -0
- package/src/hiring-candidate-fields.js +92 -0
- package/src/hiring-context.d.ts +64 -0
- package/src/hiring-context.js +412 -0
- package/src/hiring-language-extensions.d.ts +19 -0
- package/src/hiring-language-extensions.js +139 -0
- package/src/hiring-languages.js +135 -0
- package/src/hiring-location-fields.d.ts +3 -0
- package/src/hiring-location-fields.js +53 -0
- package/src/hiring-professions-ro.js +191 -0
- package/src/hiring-professions.js +437 -0
- package/src/hiring-requirements.d.ts +16 -0
- package/src/hiring-requirements.js +170 -0
- package/src/hiring-salary-context.d.ts +27 -0
- package/src/hiring-salary-context.js +76 -0
- package/src/hiring-semantics.d.ts +29 -0
- package/src/hiring-semantics.js +218 -0
- package/src/hiring-skills.d.ts +21 -0
- package/src/hiring-skills.js +315 -0
- package/src/hiring-source-aliases.d.ts +32 -0
- package/src/hiring-source-aliases.js +186 -0
- package/src/hiring-source-semantics.d.ts +31 -0
- package/src/hiring-source-semantics.js +293 -0
- package/src/hiring-temporal.d.ts +15 -0
- package/src/hiring-temporal.js +67 -0
- package/src/hiring-vacancy-fields.d.ts +5 -0
- package/src/hiring-vacancy-fields.js +23 -0
- package/src/hiring-work-semantics.d.ts +34 -0
- package/src/hiring-work-semantics.js +72 -0
- package/src/hiring.js +180 -0
- package/src/housing-address.d.ts +18 -0
- package/src/housing-address.js +210 -0
- package/src/housing-context.d.ts +36 -0
- package/src/housing-context.js +183 -0
- package/src/housing-features.js +30 -0
- package/src/housing-intent.d.ts +9 -0
- package/src/housing-intent.js +132 -0
- package/src/housing-listing-fields.js +131 -0
- package/src/housing-money.d.ts +7 -0
- package/src/housing-money.js +102 -0
- package/src/housing-source-aliases.d.ts +8 -0
- package/src/housing-source-aliases.js +51 -0
- package/src/housing-structured.d.ts +55 -0
- package/src/housing-structured.js +219 -0
- package/src/housing.js +200 -0
- package/src/index.js +44 -0
- package/src/kz-location-extensions.js +307 -0
- package/src/landmarks.js +21 -0
- package/src/lexicon-core.js +58 -0
- package/src/location-merge.js +106 -0
- package/src/locations.js +456 -0
- package/src/money-core.d.ts +10 -0
- package/src/money-core.js +81 -0
- package/src/money-lexicon.d.ts +5 -0
- package/src/money-lexicon.js +79 -0
- package/src/money.js +154 -0
- package/src/normalization.js +384 -0
- package/src/odesa-metropolitan.js +145 -0
- package/src/romania-geography.js +47 -0
- package/src/tashkent-colloquial.js +72 -0
- package/src/tashkent-housing-geography.d.ts +38 -0
- package/src/tashkent-housing-geography.js +211 -0
- package/src/tashkent-pois.js +215 -0
- package/src/tashkent-residential-complexes.js +272 -0
- package/src/ua-location-extensions-major.js +118 -0
- package/src/ua-location-extensions-metro.js +9 -0
- package/src/ua-location-extensions-regional.js +246 -0
- package/src/ua-secondary-cities.d.ts +2 -0
- package/src/ua-secondary-cities.js +57 -0
- package/src/ukraine.js +75 -0
- package/src/uz-location-extensions.js +259 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { deepFreeze, lexiconEntity } from './lexicon-core.js';
|
|
2
|
+
import { aliasesOf, escapeRegex, findAllCanonical, findCanonical } from './normalization.js';
|
|
3
|
+
import { HIRING_INTENT, BENEFIT_TERMS } from './hiring.js';
|
|
4
|
+
import { HIRING_NEGATIVE_INTENT } from './hiring-advanced.js';
|
|
5
|
+
import { LANGUAGES, LANGUAGE_LEVELS, LANGUAGE_CONTEXT_TERMS } from './hiring-languages.js';
|
|
6
|
+
import { matchProfessions } from './hiring-professions.js';
|
|
7
|
+
|
|
8
|
+
const group = (canonical, aliases, extra = {}) => lexiconEntity(canonical, aliases, extra);
|
|
9
|
+
const matchCanonicals = (text, entries) => [...new Set(findAllCanonical(text, entries).map(({ canonical }) => canonical).filter(Boolean))];
|
|
10
|
+
const has = (text, entries) => findCanonical(text, entries, { partial: true })?.canonical || null;
|
|
11
|
+
|
|
12
|
+
/** Generic requirement relation. Entity-specific parsers attach it by distance. */
|
|
13
|
+
export const REQUIREMENT_TERMS = Object.freeze([
|
|
14
|
+
group('notRequired', {
|
|
15
|
+
ru: ['не требуется', 'не обязательно', 'не нужен', 'не нужна', 'без необходимости'],
|
|
16
|
+
en: ['not required', 'not mandatory', 'optional', 'no requirement'],
|
|
17
|
+
uk: ['не потрібно', 'не обов’язково', "не обов'язково"],
|
|
18
|
+
ro: ['nu este necesar', 'nu este obligatoriu', 'opțional', 'optional'],
|
|
19
|
+
uzLatn: ['talab qilinmaydi', 'shart emas', 'kerak emas'],
|
|
20
|
+
uzCyrl: ['талаб қилинмайди', 'шарт эмас', 'керак эмас'],
|
|
21
|
+
kk: ['талап етілмейді', 'міндетті емес', 'қажет емес'],
|
|
22
|
+
}),
|
|
23
|
+
group('required', {
|
|
24
|
+
ru: ['обязательно', 'требуется', 'необходимо', 'нужно', 'должен знать', 'должна знать'],
|
|
25
|
+
en: ['required', 'must have', 'must know', 'must speak', 'mandatory', 'essential'],
|
|
26
|
+
uk: ['обов’язково', "обов'язково", 'потрібно', 'необхідно', 'вимагається'],
|
|
27
|
+
ro: ['obligatoriu', 'necesar', 'este necesar', 'se cere'],
|
|
28
|
+
uzLatn: ['talab qilinadi', 'shart', 'kerak', 'zarur'],
|
|
29
|
+
uzCyrl: ['талаб қилинади', 'шарт', 'керак', 'зарур'],
|
|
30
|
+
kk: ['талап етіледі', 'міндетті', 'қажет'],
|
|
31
|
+
}),
|
|
32
|
+
group('preferred', {
|
|
33
|
+
ru: ['желательно', 'будет плюсом', 'будет преимуществом', 'приветствуется'],
|
|
34
|
+
en: ['preferred', 'nice to have', 'a plus', 'an advantage', 'desirable'],
|
|
35
|
+
uk: ['бажано', 'буде плюсом', 'буде перевагою', 'вітається'],
|
|
36
|
+
ro: ['de preferat', 'constituie un avantaj', 'este un plus'],
|
|
37
|
+
uzLatn: ['afzal', 'plus boladi', "plus bo'ladi", 'yaxshi boladi'],
|
|
38
|
+
uzCyrl: ['афзал', 'плюс бўлади', 'яхши бўлади'],
|
|
39
|
+
kk: ['артықшылық', 'болғаны дұрыс', 'қалаулы'],
|
|
40
|
+
}),
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
export const RESPONSIBILITY_TERMS = Object.freeze([
|
|
44
|
+
group('responsibilities', {
|
|
45
|
+
ru: ['обязанности', 'задачи', 'чем предстоит заниматься', 'что нужно делать', 'функционал'],
|
|
46
|
+
en: ['responsibilities', 'duties', 'what you will do', 'what you’ll do', 'role responsibilities'],
|
|
47
|
+
uk: ['обов’язки', "обов'язки", 'задачі', 'чим будете займатися'],
|
|
48
|
+
ro: ['responsabilități', 'responsabilitati', 'sarcini'],
|
|
49
|
+
uzLatn: ['vazifalar', 'majburiyatlar', 'nima qilasiz'],
|
|
50
|
+
uzCyrl: ['вазифалар', 'мажбуриятлар'],
|
|
51
|
+
kk: ['міндеттер', 'тапсырмалар'],
|
|
52
|
+
}),
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
export const OFFER_TERMS = Object.freeze([
|
|
56
|
+
group('offer', {
|
|
57
|
+
ru: ['мы предлагаем', 'предлагаем', 'условия', 'что предлагаем', 'соцпакет'],
|
|
58
|
+
en: ['we offer', 'what we offer', 'benefits', 'perks', 'compensation and benefits'],
|
|
59
|
+
uk: ['ми пропонуємо', 'пропонуємо', 'умови', 'що пропонуємо'],
|
|
60
|
+
ro: ['oferim', 'ce oferim', 'beneficii'],
|
|
61
|
+
uzLatn: ['biz taklif qilamiz', 'taklif qilamiz', 'sharoitlar'],
|
|
62
|
+
uzCyrl: ['биз таклиф қиламиз', 'таклиф қиламиз', 'шароитлар'],
|
|
63
|
+
kk: ['біз ұсынамыз', 'ұсынамыз', 'жағдайлар'],
|
|
64
|
+
}),
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
export const CONTACT_TERMS = Object.freeze([
|
|
68
|
+
group('telegram', { ru: ['писать в telegram', 'пишите в телеграм', 'телеграм для связи'], en: ['contact on telegram', 'message on telegram'], uk: ['пишіть у telegram'], ro: ['scrieți pe telegram'], uzLatn: ['telegramga yozing'], uzCyrl: ['телеграмга ёзинг'], kk: ['telegram-ға жазыңыз'] }),
|
|
69
|
+
group('whatsapp', { ru: ['писать в whatsapp', 'ватсап для связи'], en: ['contact on whatsapp', 'message on whatsapp'], uk: ['пишіть у whatsapp'], ro: ['scrieți pe whatsapp'], uzLatn: ['whatsappga yozing'], uzCyrl: ['whatsappга ёзинг'], kk: ['whatsapp-қа жазыңыз'] }),
|
|
70
|
+
group('phone', { ru: ['звонить', 'позвонить', 'по телефону'], en: ['call', 'phone us', 'by phone'], uk: ['дзвонити', 'телефонуйте'], ro: ['sunați', 'telefon'], uzLatn: ['qongiroq qiling', "qo'ng'iroq qiling"], uzCyrl: ['қўнғироқ қилинг'], kk: ['қоңырау шалыңыз'] }),
|
|
71
|
+
group('recruiter', { ru: ['contact hr', 'связаться с hr', 'связаться с рекрутером'], en: ['contact hr', 'contact recruiter'], uk: ['зв’язатися з рекрутером'], ro: ['contactați recruiterul'], uzLatn: ['hr bilan boglaning'], uzCyrl: ['hr билан боғланинг'], kk: ['рекрутермен байланысыңыз'] }),
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
export const APPLICATION_TERMS = Object.freeze([
|
|
75
|
+
group('resumeRequired', { ru: ['отправить резюме', 'прислать резюме', 'резюме обязательно'], en: ['send resume', 'submit resume', 'resume required', 'send cv', 'submit cv'], uk: ['надіслати резюме', 'резюме обов’язкове'], ro: ['trimite cv', 'cv obligatoriu'], uzLatn: ['rezyume yuboring'], uzCyrl: ['резюме юборинг'], kk: ['түйіндеме жіберіңіз'] }),
|
|
76
|
+
group('portfolioRequired', { ru: ['портфолио обязательно', 'прислать портфолио'], en: ['portfolio required', 'submit portfolio', 'send portfolio'], uk: ['портфоліо обов’язкове'], ro: ['portofoliu obligatoriu'], uzLatn: ['portfolio yuboring'], uzCyrl: ['портфолио юборинг'], kk: ['портфолио жіберіңіз'] }),
|
|
77
|
+
group('coverLetter', { ru: ['сопроводительное письмо'], en: ['cover letter'], uk: ['супровідний лист'], ro: ['scrisoare de intenție'], uzLatn: ['motivatsion xat'], uzCyrl: ['мотивацион хат'], kk: ['ілеспе хат'] }),
|
|
78
|
+
group('testTask', { ru: ['тестовое задание', 'тестовая задача'], en: ['test task', 'take-home assignment', 'coding assignment'], uk: ['тестове завдання'], ro: ['test tehnic', 'temă de test'], uzLatn: ['test topshirigi'], uzCyrl: ['тест топшириғи'], kk: ['тест тапсырмасы'] }),
|
|
79
|
+
group('interview', { ru: ['собеседование', 'интервью с hr'], en: ['interview', 'screening call'], uk: ['співбесіда'], ro: ['interviu'], uzLatn: ['suhbat'], uzCyrl: ['суҳбат'], kk: ['сұхбат'] }),
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
export const COMPANY_TERMS = Object.freeze([
|
|
83
|
+
group('company', { ru: ['компания', 'работодатель', 'бренд', 'сеть компаний'], en: ['company', 'employer', 'brand'], uk: ['компанія', 'роботодавець', 'бренд'], ro: ['companie', 'angajator', 'brand'], uzLatn: ['kompaniya', 'ish beruvchi'], uzCyrl: ['компания', 'иш берувчи'], kk: ['компания', 'жұмыс беруші'] }),
|
|
84
|
+
group('startup', { ru: ['стартап'], en: ['startup', 'start-up'], uk: ['стартап'], ro: ['startup'], uzLatn: ['startap'], uzCyrl: ['стартап'], kk: ['стартап'] }),
|
|
85
|
+
group('agency', { ru: ['агентство', 'кадровое агентство'], en: ['agency', 'recruitment agency'], uk: ['агенція', 'рекрутингова агенція'], ro: ['agenție', 'agenție de recrutare'], uzLatn: ['agentlik'], uzCyrl: ['агентлик'], kk: ['агенттік'] }),
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
export const LOCATION_CONTEXT_TERMS = Object.freeze([
|
|
89
|
+
group('workplace', { ru: ['место работы', 'офис находится', 'адрес офиса', 'работа в'], en: ['work location', 'office located', 'office address', 'based in'], uk: ['місце роботи', 'офіс знаходиться'], ro: ['locul de muncă', 'adresa biroului'], uzLatn: ['ish joyi', 'ofis manzili'], uzCyrl: ['иш жойи', 'офис манзили'], kk: ['жұмыс орны', 'кеңсе мекенжайы'] }),
|
|
90
|
+
group('commute', { ru: ['трансфер до офиса', 'развозка сотрудников'], en: ['office shuttle', 'employee shuttle', 'commute support'], uk: ['трансфер до офісу'], ro: ['transport la birou'], uzLatn: ['xodimlar transporti'], uzCyrl: ['ходимлар транспорти'], kk: ['қызметкерлер тасымалы'] }),
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
export const WORK_AUTHORIZATION_TERMS = Object.freeze([
|
|
94
|
+
group('sponsorshipOffered', { ru: ['визовая поддержка', 'спонсируем рабочую визу', 'оформляем рабочую визу'], en: ['visa sponsorship available', 'visa sponsorship provided', 'we sponsor visas', 'sponsorship available'], uk: ['візова підтримка', 'спонсоруємо робочу візу'], ro: ['sponsorizare viză', 'sponsorizare pentru viză'], uzLatn: ['viza yordami'], uzCyrl: ['виза ёрдами'], kk: ['визаға демеушілік'] }),
|
|
95
|
+
group('noSponsorship', { ru: ['без визовой поддержки', 'визу не спонсируем', 'спонсорства визы нет'], en: ['no visa sponsorship', 'visa sponsorship is not available', 'we do not sponsor', 'unable to sponsor', 'cannot sponsor', 'no sponsorship'], uk: ['без візової підтримки', 'візу не спонсоруємо'], ro: ['fără sponsorizare pentru viză'], uzLatn: ['viza homiyligi yoq'], uzCyrl: ['виза ҳомийлиги йўқ'], kk: ['виза демеушілігі жоқ'] }),
|
|
96
|
+
group('workPermitRequired', { ru: ['разрешение на работу обязательно', 'нужно разрешение на работу'], en: ['work permit required', 'must have work authorization', 'must be authorized to work', 'right to work required'], uk: ['дозвіл на роботу обов’язковий'], ro: ['permis de muncă obligatoriu'], uzLatn: ['ishlash ruxsati kerak'], uzCyrl: ['ишлаш рухсати керак'], kk: ['жұмыс істеуге рұқсат қажет'] }),
|
|
97
|
+
group('citizenshipRequired', { ru: ['только граждане', 'гражданство обязательно'], en: ['citizenship required', 'citizens only'], uk: ['лише громадяни', 'громадянство обов’язкове'], ro: ['cetățenie obligatorie'], uzLatn: ['faqat fuqarolar'], uzCyrl: ['фақат фуқаролар'], kk: ['тек азаматтар'] }),
|
|
98
|
+
group('residencePermit', { ru: ['внж', 'вид на жительство'], en: ['residence permit', 'residency permit'], uk: ['посвідка на проживання'], ro: ['permis de ședere'], uzLatn: ['yashash ruxsati'], uzCyrl: ['яшаш рухсати'], kk: ['тұруға ықтиярхат'] }),
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
export const HIRING_AVAILABILITY_TERMS = Object.freeze([
|
|
102
|
+
group('urgent', { ru: ['срочно нужен', 'срочно требуется', 'срочный набор'], en: ['urgent hire', 'hiring urgently', 'urgent opening'], uk: ['терміново потрібен'], ro: ['angajare urgentă'], uzLatn: ['zudlik bilan xodim kerak'], uzCyrl: ['зудлик билан ходим керак'], kk: ['шұғыл қызметкер керек'] }),
|
|
103
|
+
group('immediateStart', { ru: ['выход завтра', 'приступить сразу', 'выход сразу'], en: ['immediate start', 'start immediately', 'asap start'], uk: ['вийти одразу', 'почати одразу'], ro: ['începere imediată'], uzLatn: ['darhol ish boshlash'], uzCyrl: ['дарҳол иш бошлаш'], kk: ['бірден бастау'] }),
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
export const TRAVEL_TERMS = Object.freeze([
|
|
107
|
+
group('required', { ru: ['готовность к командировкам', 'командировки обязательны', 'разъездной характер работы'], en: ['travel required', 'business travel required', 'willingness to travel'], uk: ['готовність до відряджень'], ro: ['disponibilitate pentru deplasări'], uzLatn: ['xizmat safariga tayyor'], uzCyrl: ['хизмат сафарига тайёр'], kk: ['іссапарға дайын'] }),
|
|
108
|
+
group('none', { ru: ['без командировок', 'командировок нет'], en: ['no travel', 'no business travel'], uk: ['без відряджень'], ro: ['fără deplasări'], uzLatn: ['xizmat safari yoq'], uzCyrl: ['хизмат сафари йўқ'], kk: ['іссапар жоқ'] }),
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
export const RELOCATION_CONTEXT_TERMS = Object.freeze([
|
|
112
|
+
group('offered', { ru: ['релокация предоставляется', 'помощь с переездом', 'релокационный пакет'], en: ['relocation provided', 'relocation assistance', 'relocation package'], uk: ['релокація надається', 'допомога з переїздом'], ro: ['pachet de relocare', 'asistență la relocare'], uzLatn: ['kochishga yordam', "ko'chishga yordam"], uzCyrl: ['кўчишга ёрдам'], kk: ['көшуге көмек'] }),
|
|
113
|
+
group('required', { ru: ['требуется релокация', 'необходим переезд'], en: ['relocation required', 'must relocate'], uk: ['потрібна релокація'], ro: ['relocare obligatorie'], uzLatn: ['kochish talab qilinadi'], uzCyrl: ['кўчиш талаб қилинади'], kk: ['көшу қажет'] }),
|
|
114
|
+
group('notOffered', { ru: ['релокация не предоставляется', 'без релокации'], en: ['no relocation', 'relocation not provided'], uk: ['релокація не надається'], ro: ['fără relocare'], uzLatn: ['kochish yordami yoq'], uzCyrl: ['кўчиш ёрдами йўқ'], kk: ['көшу көмегі жоқ'] }),
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
export const BONUS_TERMS = Object.freeze([
|
|
118
|
+
group('kpi', { ru: ['kpi', 'бонус по kpi'], en: ['kpi bonus', 'performance bonus'], uk: ['бонус за kpi'], ro: ['bonus kpi'], uzLatn: ['kpi bonus'], uzCyrl: ['kpi бонус'], kk: ['kpi бонус'] }),
|
|
119
|
+
group('salesCommission', { ru: ['процент от продаж', 'процент с продаж'], en: ['sales commission', 'commission on sales'], uk: ['відсоток від продажів'], ro: ['comision din vânzări'], uzLatn: ['sotuvdan foiz'], uzCyrl: ['сотувдан фоиз'], kk: ['сатудан пайыз'] }),
|
|
120
|
+
group('bonus', { ru: ['бонус', 'премия'], en: ['bonus', 'incentive'], uk: ['бонус', 'премія'], ro: ['bonus', 'primă'], uzLatn: ['bonus', 'mukofot'], uzCyrl: ['бонус', 'мукофот'], kk: ['бонус', 'сыйақы'] }),
|
|
121
|
+
group('tips', { ru: ['чаевые'], en: ['tips', 'gratuities'], uk: ['чайові'], ro: ['bacșiș'], uzLatn: ['choychaqa'], uzCyrl: ['чойчақа'], kk: ['шайпұл'] }),
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
export const CONTRACT_CONTEXT_TERMS = Object.freeze([
|
|
125
|
+
group('officialEmployment', { ru: ['официальное оформление', 'оформление по тк', 'по трудовой книжке'], en: ['official employment', 'formal employment'], uk: ['офіційне працевлаштування'], ro: ['angajare oficială'], uzLatn: ['rasmiy ishga olish'], uzCyrl: ['расмий ишга олиш'], kk: ['ресми жұмысқа орналастыру'] }),
|
|
126
|
+
group('employmentContract', { ru: ['трудовой договор'], en: ['employment contract'], uk: ['трудовий договір'], ro: ['contract de muncă'], uzLatn: ['mehnat shartnomasi'], uzCyrl: ['меҳнат шартномаси'], kk: ['еңбек шарты'] }),
|
|
127
|
+
group('civilContract', { ru: ['гпх', 'гражданско-правовой договор'], en: ['civil contract'], uk: ['цивільно-правовий договір'], ro: ['contract civil'], uzLatn: ['fuqarolik shartnomasi'], uzCyrl: ['фуқаролик шартномаси'], kk: ['азаматтық шарт'] }),
|
|
128
|
+
group('contractor', { ru: ['самозанятость', 'ип', 'контрактор'], en: ['contractor', 'independent contractor', 'b2b'], uk: ['фоп', 'контрактор'], ro: ['contractor', 'pfa', 'b2b'], uzLatn: ['yakka tartibdagi tadbirkor'], uzCyrl: ['якка тартибдаги тадбиркор'], kk: ['жеке кәсіпкер'] }),
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
export const VACANCY_STATUS_TERMS = Object.freeze([
|
|
132
|
+
group('closed', { ru: ['вакансия закрыта', 'позиция закрыта', 'набор завершён', 'набор завершен'], en: ['vacancy closed', 'position closed', 'role closed'], uk: ['вакансія закрита', 'позиція закрита'], ro: ['poziție închisă'], uzLatn: ['vakansiya yopildi'], uzCyrl: ['вакансия ёпилди'], kk: ['вакансия жабылды'] }),
|
|
133
|
+
group('filled', { ru: ['сотрудник найден', 'уже нашли сотрудника'], en: ['position filled', 'role filled', 'candidate hired'], uk: ['працівника знайдено'], ro: ['post ocupat'], uzLatn: ['xodim topildi'], uzCyrl: ['ходим топилди'], kk: ['қызметкер табылды'] }),
|
|
134
|
+
group('frozen', { ru: ['позиция заморожена', 'вакансия заморожена'], en: ['position frozen', 'hiring paused', 'role on hold'], uk: ['позиція заморожена'], ro: ['poziție înghețată'], uzLatn: ['vakansiya vaqtincha toxtatilgan'], uzCyrl: ['вакансия вақтинча тўхтатилган'], kk: ['вакансия уақытша тоқтатылды'] }),
|
|
135
|
+
group('open', { ru: ['вакансия открыта', 'позиция открыта', 'набор открыт'], en: ['open position', 'open role', 'now hiring'], uk: ['вакансія відкрита'], ro: ['poziție deschisă'], uzLatn: ['ochiq vakansiya'], uzCyrl: ['очиқ вакансия'], kk: ['ашық вакансия'] }),
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
export const CANDIDATE_STATUS_TERMS = Object.freeze([
|
|
139
|
+
group('activelyLooking', { ru: ['активно ищу работу', 'в активном поиске'], en: ['actively looking', 'actively seeking work'], uk: ['активно шукаю роботу'], ro: ['caut activ un job'], uzLatn: ['faol ish qidiryapman'], uzCyrl: ['фаол иш қидиряпман'], kk: ['белсенді жұмыс іздеп жүрмін'] }),
|
|
140
|
+
group('openToOffers', { ru: ['открыт к предложениям', 'открыта к предложениям', 'рассматриваю предложения'], en: ['open to offers', 'open to opportunities'], uk: ['відкритий до пропозицій', 'розглядаю пропозиції'], ro: ['deschis la oferte'], uzLatn: ['takliflarga ochiqman'], uzCyrl: ['таклифларга очиқман'], kk: ['ұсыныстарға ашықпын'] }),
|
|
141
|
+
group('notLooking', { ru: ['пока не ищу', 'не ищу работу'], en: ['not looking', 'not seeking work'], uk: ['не шукаю роботу'], ro: ['nu caut de lucru'], uzLatn: ['ish qidirmayapman'], uzCyrl: ['иш қидирмаяпман'], kk: ['жұмыс іздемеймін'] }),
|
|
142
|
+
group('employed', { ru: ['сейчас работаю', 'работаю, но рассматриваю', 'трудоустроен', 'трудоустроена'], en: ['currently employed', 'employed but open'], uk: ['зараз працюю'], ro: ['angajat în prezent'], uzLatn: ['hozir ishlayman'], uzCyrl: ['ҳозир ишлайман'], kk: ['қазір жұмыс істеймін'] }),
|
|
143
|
+
group('availableImmediately', { ru: ['готов приступить сразу', 'готова приступить сразу', 'могу выйти сразу', 'могу выйти завтра'], en: ['available immediately', 'can start immediately'], uk: ['можу вийти одразу'], ro: ['disponibil imediat'], uzLatn: ['darhol ish boshlay olaman'], uzCyrl: ['дарҳол иш бошлай оламан'], kk: ['бірден бастай аламын'] }),
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
export const CANDIDATE_PREFERENCE_TERMS = Object.freeze([
|
|
147
|
+
group('remoteOnly', { ru: ['только удалённая работа', 'только удаленка', 'только удалёнка'], en: ['remote only', 'only remote work'], uk: ['тільки віддалена робота'], ro: ['doar remote'], uzLatn: ['faqat masofadan'], uzCyrl: ['фақат масофадан'], kk: ['тек қашықтан'] }),
|
|
148
|
+
group('fullTimeOnly', { ru: ['только полный день', 'только полная занятость'], en: ['full time only', 'full-time only'], uk: ['тільки повна зайнятість'], ro: ['doar normă întreagă'], uzLatn: ['faqat toliq stavka'], uzCyrl: ['фақат тўлиқ ставка'], kk: ['тек толық жұмыс күні'] }),
|
|
149
|
+
group('noNightShift', { ru: ['не готов к ночным сменам', 'не готова к ночным сменам', 'без ночных смен'], en: ['no night shifts', 'not available for night shifts'], uk: ['без нічних змін'], ro: ['fără ture de noapte'], uzLatn: ['tungi smenasiz'], uzCyrl: ['тунги сменасиз'], kk: ['түнгі ауысымсыз'] }),
|
|
150
|
+
group('noTravel', { ru: ['без командировок', 'командировки не рассматриваю'], en: ['no travel', 'not open to travel'], uk: ['без відряджень'], ro: ['fără deplasări'], uzLatn: ['xizmat safarisiz'], uzCyrl: ['хизмат сафарисиз'], kk: ['іссапарсыз'] }),
|
|
151
|
+
group('noRelocation', { ru: ['релокацию не рассматриваю', 'не готов к релокации', 'не готова к релокации'], en: ['no relocation', 'not open to relocation'], uk: ['релокацію не розглядаю'], ro: ['nu doresc relocare'], uzLatn: ['kochishni korib chiqmayman'], uzCyrl: ['кўчишни кўриб чиқмайман'], kk: ['көшуге дайын емеспін'] }),
|
|
152
|
+
group('noSales', { ru: ['не рассматриваю продажи', 'продажи не рассматриваю'], en: ['not interested in sales', 'no sales roles'], uk: ['не розглядаю продажі'], ro: ['nu doresc vânzări'], uzLatn: ['sotuv ishini xohlamayman'], uzCyrl: ['сотув ишини хоҳламайман'], kk: ['сату жұмысын қарастырмаймын'] }),
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
export const EMPLOYMENT_HISTORY_TERMS = Object.freeze([
|
|
156
|
+
group('current', { ru: ['работаю в', 'текущее место работы', 'текущая компания', 'сейчас работаю'], en: ['currently work at', 'current company', 'current role'], uk: ['працюю в', 'поточна компанія'], ro: ['lucrez în prezent la', 'compania actuală'], uzLatn: ['hozir ishlayman', 'hozirgi kompaniya'], uzCyrl: ['ҳозир ишлайман', 'ҳозирги компания'], kk: ['қазір жұмыс істеймін', 'қазіргі компания'] }),
|
|
157
|
+
group('previous', { ru: ['работал в', 'работала в', 'последнее место работы', 'занимал должность', 'занимала должность', 'ушёл из', 'ушла из'], en: ['worked at', 'previous company', 'previous role', 'formerly worked'], uk: ['працював у', 'працювала у', 'попереднє місце роботи'], ro: ['am lucrat la', 'compania anterioară'], uzLatn: ['ilgari ishlaganman', 'oldingi ish joyi'], uzCyrl: ['илгари ишлаганман', 'олдинги иш жойи'], kk: ['бұрын жұмыс істедім', 'алдыңғы жұмыс орны'] }),
|
|
158
|
+
group('desired', { ru: ['ищу работу', 'хочу работать', 'интересует позиция', 'рассматриваю позицию'], en: ['looking for a role', 'seeking a role', 'interested in a role'], uk: ['шукаю роботу', 'цікавить позиція'], ro: ['caut un rol', 'mă interesează poziția'], uzLatn: ['ish qidiryapman', 'lavozim qidiryapman'], uzCyrl: ['иш қидиряпман', 'лавозим қидиряпман'], kk: ['жұмыс іздеймін', 'лауазым іздеймін'] }),
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
export const EDUCATION_CONTEXT_TERMS = Object.freeze([
|
|
162
|
+
group('required', { ru: ['высшее образование обязательно', 'образование обязательно'], en: ['degree required', 'education required'], uk: ['вища освіта обов’язкова'], ro: ['studii obligatorii'], uzLatn: ['oliy malumot shart'], uzCyrl: ['олий маълумот шарт'], kk: ['жоғары білім міндетті'] }),
|
|
163
|
+
group('preferred', { ru: ['желательно техническое образование', 'образование будет плюсом'], en: ['degree preferred', 'education preferred'], uk: ['освіта бажана'], ro: ['studii de preferat'], uzLatn: ['malumot afzal'], uzCyrl: ['маълумот афзал'], kk: ['білім болғаны дұрыс'] }),
|
|
164
|
+
group('notRequired', { ru: ['образование не имеет значения', 'диплом не требуется'], en: ['degree not required', 'education not required'], uk: ['диплом не потрібен'], ro: ['diploma nu este necesară'], uzLatn: ['diplom shart emas'], uzCyrl: ['диплом шарт эмас'], kk: ['диплом қажет емес'] }),
|
|
165
|
+
group('candidateHas', { ru: ['имею высшее образование', 'окончил университет', 'окончила университет'], en: ['i have a degree', 'graduated from university'], uk: ['маю вищу освіту', 'закінчив університет'], ro: ['am studii superioare', 'am absolvit universitatea'], uzLatn: ['oliy malumotim bor', 'universitetni bitirganman'], uzCyrl: ['олий маълумотим бор', 'университетни битирганман'], kk: ['жоғары білімім бар', 'университет бітірдім'] }),
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
export const DRIVER_CONTEXT_TERMS = Object.freeze([
|
|
169
|
+
group('licenseRequired', { ru: ['требуются права категории', 'водительское удостоверение обязательно'], en: ['driving licence required', 'driver license required'], uk: ['водійське посвідчення обов’язкове'], ro: ['permis de conducere obligatoriu'], uzLatn: ['haydovchilik guvohnomasi kerak'], uzCyrl: ['ҳайдовчилик гувоҳномаси керак'], kk: ['жүргізуші куәлігі қажет'] }),
|
|
170
|
+
group('carRequired', { ru: ['наличие автомобиля обязательно', 'с личным авто обязательно'], en: ['own car required', 'personal vehicle required'], uk: ['власне авто обов’язкове'], ro: ['mașină proprie obligatorie'], uzLatn: ['shaxsiy avtomobil kerak'], uzCyrl: ['шахсий автомобил керак'], kk: ['жеке көлік қажет'] }),
|
|
171
|
+
group('candidateHasLicense', { ru: ['есть права категории', 'имею водительские права'], en: ['i have a driving licence', 'drivers license category'], uk: ['маю водійські права'], ro: ['am permis de conducere'], uzLatn: ['haydovchilik guvohnomam bor'], uzCyrl: ['ҳайдовчилик гувоҳномам бор'], kk: ['жүргізуші куәлігім бар'] }),
|
|
172
|
+
group('candidateHasCar', { ru: ['есть личный автомобиль', 'личный автомобиль имеется'], en: ['i have my own car', 'own vehicle'], uk: ['маю власне авто'], ro: ['am mașină proprie'], uzLatn: ['shaxsiy avtomobilim bor'], uzCyrl: ['шахсий автомобилм бор'], kk: ['жеке көлігім бар'] }),
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
export const HIRING_SECTION_MARKERS = Object.freeze({
|
|
176
|
+
vacancy: group('vacancy', { ru: ['вакансия', 'должность', 'позиция'], en: ['vacancy', 'position', 'role'], uk: ['вакансія', 'посада', 'позиція'], ro: ['vacanță', 'poziție', 'rol'], uzLatn: ['vakansiya', 'lavozim'], uzCyrl: ['вакансия', 'лавозим'], kk: ['вакансия', 'лауазым'] }),
|
|
177
|
+
company: group('company', { ru: ['компания', 'работодатель'], en: ['company', 'employer'], uk: ['компанія', 'роботодавець'], ro: ['companie', 'angajator'], uzLatn: ['kompaniya', 'ish beruvchi'], uzCyrl: ['компания', 'иш берувчи'], kk: ['компания', 'жұмыс беруші'] }),
|
|
178
|
+
salary: group('salary', { ru: ['зарплата', 'зп', 'оплата'], en: ['salary', 'compensation', 'pay'], uk: ['зарплата', 'оплата'], ro: ['salariu', 'plată'], uzLatn: ['maosh', 'oylik'], uzCyrl: ['маош', 'ойлик'], kk: ['жалақы', 'айлық'] }),
|
|
179
|
+
location: group('location', { ru: ['локация', 'место работы', 'адрес'], en: ['location', 'work location'], uk: ['локація', 'місце роботи'], ro: ['locație', 'locul de muncă'], uzLatn: ['manzil', 'ish joyi'], uzCyrl: ['манзил', 'иш жойи'], kk: ['мекенжай', 'жұмыс орны'] }),
|
|
180
|
+
schedule: group('schedule', { ru: ['график', 'график работы'], en: ['schedule', 'work schedule'], uk: ['графік'], ro: ['program'], uzLatn: ['grafik'], uzCyrl: ['график'], kk: ['кесте'] }),
|
|
181
|
+
requirements: group('requirements', { ru: ['требования', 'кого ищем'], en: ['requirements', 'what we expect', 'who we are looking for'], uk: ['вимоги', 'кого шукаємо'], ro: ['cerințe', 'cerinte'], uzLatn: ['talablar'], uzCyrl: ['талаблар'], kk: ['талаптар'] }),
|
|
182
|
+
responsibilities: group('responsibilities', { ru: ['обязанности', 'задачи'], en: ['responsibilities', 'duties'], uk: ['обов’язки', 'задачі'], ro: ['responsabilități', 'sarcini'], uzLatn: ['vazifalar'], uzCyrl: ['вазифалар'], kk: ['міндеттер'] }),
|
|
183
|
+
offer: group('offer', { ru: ['условия', 'мы предлагаем', 'что предлагаем'], en: ['we offer', 'benefits', 'perks'], uk: ['умови', 'ми пропонуємо'], ro: ['oferim', 'beneficii'], uzLatn: ['sharoitlar', 'taklif qilamiz'], uzCyrl: ['шароитлар', 'таклиф қиламиз'], kk: ['жағдайлар', 'ұсынамыз'] }),
|
|
184
|
+
contacts: group('contacts', { ru: ['контакты', 'связь'], en: ['contacts', 'contact'], uk: ['контакти'], ro: ['contact'], uzLatn: ['aloqa'], uzCyrl: ['алоқа'], kk: ['байланыс'] }),
|
|
185
|
+
application: group('application', { ru: ['как откликнуться', 'отклик'], en: ['how to apply', 'application'], uk: ['як відгукнутися', 'відгук'], ro: ['cum aplici', 'aplicare'], uzLatn: ['murojaat'], uzCyrl: ['мурожаат'], kk: ['өтініш беру'] }),
|
|
186
|
+
languages: group('languages', { ru: ['языки', 'знание языков'], en: ['languages', 'language skills'], uk: ['мови'], ro: ['limbi'], uzLatn: ['tillar'], uzCyrl: ['тиллар'], kk: ['тілдер'] }),
|
|
187
|
+
skills: group('skills', { ru: ['навыки', 'стек'], en: ['skills', 'tech stack'], uk: ['навички'], ro: ['competențe'], uzLatn: ['konikmalar', "ko'nikmalar", 'texnologiyalar'], uzCyrl: ['кўникмалар'], kk: ['дағдылар'] }),
|
|
188
|
+
experience: group('experience', { ru: ['опыт', 'опыт работы'], en: ['experience', 'work experience'], uk: ['досвід', 'досвід роботи'], ro: ['experiență'], uzLatn: ['tajriba', 'ish tajribasi'], uzCyrl: ['тажриба', 'иш тажрибаси'], kk: ['тәжірибе'] }),
|
|
189
|
+
education: group('education', { ru: ['образование'], en: ['education'], uk: ['освіта'], ro: ['educație', 'studii'], uzLatn: ['malumot', "ma'lumot"], uzCyrl: ['маълумот'], kk: ['білім'] }),
|
|
190
|
+
expectations: group('expectations', { ru: ['ожидания', 'что ищу', 'желаемая должность'], en: ['expectations', 'looking for', 'desired role'], uk: ['очікування', 'бажана посада'], ro: ['așteptări', 'rol dorit'], uzLatn: ['kutilmalar', 'istalgan lavozim'], uzCyrl: ['кутилмалар'], kk: ['күтілімдер', 'қалаған лауазым'] }),
|
|
191
|
+
history: group('history', { ru: ['опыт работы', 'места работы', 'карьера'], en: ['work experience', 'employment history', 'career history'], uk: ['досвід роботи', 'місця роботи'], ro: ['experiență profesională'], uzLatn: ['ish tajribasi'], uzCyrl: ['иш тажрибаси'], kk: ['еңбек тәжірибесі'] }),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
export const HIRING_NON_CONTENT_TERMS = Object.freeze([
|
|
195
|
+
group('vacancyDigest', { ru: ['дайджест вакансий', 'подборка вакансий', 'вакансии недели'], en: ['job digest', 'vacancy digest', 'jobs roundup'], uk: ['добірка вакансій'], ro: ['selecție de joburi'], uzLatn: ['vakansiyalar toplami'], uzCyrl: ['вакансиялар тўплами'], kk: ['вакансиялар топтамасы'] }),
|
|
196
|
+
group('recruitmentAd', { ru: ['поможем подобрать сотрудника', 'услуги рекрутера', 'закроем вакансию'], en: ['recruitment services', 'we find candidates', 'staffing services'], uk: ['послуги рекрутера'], ro: ['servicii de recrutare'], uzLatn: ['rekruting xizmati'], uzCyrl: ['рекрутинг хизмати'], kk: ['рекрутинг қызметтері'] }),
|
|
197
|
+
group('jobService', { ru: ['помогу найти работу', 'подбор вакансий', 'сервис поиска работы'], en: ['job search service', 'we help you find a job'], uk: ['допоможемо знайти роботу'], ro: ['serviciu de căutare job'], uzLatn: ['ish topishga yordam'], uzCyrl: ['иш топишга ёрдам'], kk: ['жұмыс табуға көмектесеміз'] }),
|
|
198
|
+
group('spam', { ru: ['подпишись на канал', 'заработок без вложений', 'доход без вложений', 'легкий заработок'], en: ['subscribe to our channel', 'earn money without investment', 'easy money'], uk: ['підпишись на канал', 'заробіток без вкладень'], ro: ['abonează-te la canal', 'câștig fără investiții'], uzLatn: ['kanalga obuna boling', 'sarmoyasiz daromad'], uzCyrl: ['каналга обуна бўлинг'], kk: ['арнаға жазылыңыз', 'инвестициясыз табыс'] }),
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
function sectionPattern(entry) {
|
|
202
|
+
const values = aliasesOf(entry).sort((a, b) => b.length - a.length).map((value) => escapeRegex(value).replace(/\s+/g, '\\s+'));
|
|
203
|
+
return values.length ? new RegExp(`^[^\\p{L}\\p{N}]{0,8}(?:${values.join('|')})\\s*[:—-]\\s*(.*)$`, 'iu') : null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const SECTION_PATTERNS = Object.freeze(Object.fromEntries(
|
|
207
|
+
Object.entries(HIRING_SECTION_MARKERS).map(([key, entry]) => [key, sectionPattern(entry)]),
|
|
208
|
+
));
|
|
209
|
+
|
|
210
|
+
export function splitHiringSections(value) {
|
|
211
|
+
const text = String(value || '').replace(/\r/g, '');
|
|
212
|
+
const out = {};
|
|
213
|
+
let current = null;
|
|
214
|
+
for (const rawLine of text.split('\n')) {
|
|
215
|
+
const line = rawLine.trim();
|
|
216
|
+
if (!line) continue;
|
|
217
|
+
let marker = null;
|
|
218
|
+
let firstLine = '';
|
|
219
|
+
for (const [key, re] of Object.entries(SECTION_PATTERNS)) {
|
|
220
|
+
const match = re?.exec(line);
|
|
221
|
+
if (!match) continue;
|
|
222
|
+
marker = key;
|
|
223
|
+
firstLine = match[1]?.trim() || '';
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
if (marker) {
|
|
227
|
+
current = marker;
|
|
228
|
+
if (!out[current]) out[current] = [];
|
|
229
|
+
if (firstLine) out[current].push(firstLine);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (current) out[current].push(line);
|
|
233
|
+
}
|
|
234
|
+
return deepFreeze(Object.fromEntries(Object.entries(out).map(([key, lines]) => [key, lines.join('\n')])));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function nearbyRelation(text, entityMatch, relationMatches, fallback = null) {
|
|
238
|
+
let best = null;
|
|
239
|
+
let bestDistance = Infinity;
|
|
240
|
+
for (const relation of relationMatches) {
|
|
241
|
+
const before = relation.end <= entityMatch.start ? entityMatch.start - relation.end : Infinity;
|
|
242
|
+
const after = relation.start >= entityMatch.end ? relation.start - entityMatch.end : Infinity;
|
|
243
|
+
const overlap = relation.start <= entityMatch.end && relation.end >= entityMatch.start ? 0 : Infinity;
|
|
244
|
+
const distance = Math.min(before, after, overlap);
|
|
245
|
+
if (distance > 80 || distance >= bestDistance) continue;
|
|
246
|
+
best = relation.canonical;
|
|
247
|
+
bestDistance = distance;
|
|
248
|
+
}
|
|
249
|
+
return best || fallback;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function languageLevelNear(text, match) {
|
|
253
|
+
const start = Math.max(0, match.start - 34);
|
|
254
|
+
const end = Math.min(text.length, match.end + 34);
|
|
255
|
+
const window = text.slice(start, end);
|
|
256
|
+
const cefr = window.match(/(?<![\p{L}\p{N}])([ABC][12])(?![\p{L}\p{N}])/iu)?.[1]?.toUpperCase() || null;
|
|
257
|
+
if (cefr) return { level: null, cefr };
|
|
258
|
+
const qualitative = findCanonical(window, LANGUAGE_LEVELS, { partial: true })?.canonical || null;
|
|
259
|
+
return { level: qualitative, cefr: null };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function parseLanguageContext(value, { mode = null } = {}) {
|
|
263
|
+
const text = String(value || '');
|
|
264
|
+
if (!text.trim()) return [];
|
|
265
|
+
const languages = findAllCanonical(text, LANGUAGES);
|
|
266
|
+
const genericRelations = findAllCanonical(text, REQUIREMENT_TERMS);
|
|
267
|
+
const languageSpecificRelations = findAllCanonical(text, Object.values(LANGUAGE_CONTEXT_TERMS));
|
|
268
|
+
const relations = [...genericRelations, ...languageSpecificRelations].sort((a, b) => a.start - b.start);
|
|
269
|
+
|
|
270
|
+
const seen = new Set();
|
|
271
|
+
const out = [];
|
|
272
|
+
for (const language of languages) {
|
|
273
|
+
if (!language.canonical || seen.has(language.canonical)) continue;
|
|
274
|
+
seen.add(language.canonical);
|
|
275
|
+
const { level, cefr } = languageLevelNear(text, language);
|
|
276
|
+
const fallback = mode === 'candidate' ? 'candidateHas' : null;
|
|
277
|
+
out.push(deepFreeze({
|
|
278
|
+
language: language.canonical,
|
|
279
|
+
name: language.entry?.name || language.canonical,
|
|
280
|
+
relation: nearbyRelation(text, language, relations, fallback),
|
|
281
|
+
level,
|
|
282
|
+
cefr,
|
|
283
|
+
start: language.start,
|
|
284
|
+
end: language.end,
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
return Object.freeze(out);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function firstProfession(text) {
|
|
291
|
+
return matchProfessions(text, { limit: 1, allowWeak: true })[0] || null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function uniqueProfessionMatches(matches) {
|
|
295
|
+
const seen = new Set();
|
|
296
|
+
return matches.filter((item) => {
|
|
297
|
+
const key = item?.id || item?.canonical;
|
|
298
|
+
if (!key || seen.has(key)) return false;
|
|
299
|
+
seen.add(key);
|
|
300
|
+
return true;
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function contextSentences(text, regex) {
|
|
305
|
+
return text.split(/\n|(?<=[.!?])\s+/u).filter((sentence) => regex.test(sentence));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function resolveProfessionContext(value, { mode = null, title = '' } = {}) {
|
|
309
|
+
const text = String(value || '');
|
|
310
|
+
const sections = splitHiringSections(text);
|
|
311
|
+
const resolvedMode = mode || (has(text, [HIRING_INTENT.candidate]) ? 'candidate' : 'vacancy');
|
|
312
|
+
const allMatches = matchProfessions(text, { limit: 20, allowWeak: true });
|
|
313
|
+
|
|
314
|
+
if (resolvedMode === 'candidate') {
|
|
315
|
+
const desiredText = [sections.expectations, ...contextSentences(text, /ищу|шукаю|looking for|seeking|desired|желаем|цікавить|qidir|іздей/iu)].filter(Boolean).join('\n');
|
|
316
|
+
const currentText = contextSentences(text, /сейчас\s+работ|работаю\s+в|currently\s+(?:work|employed)|current\s+(?:role|company)|hozir\s+ish|қазір\s+жұмыс/iu).join('\n');
|
|
317
|
+
const previousText = [sections.history, ...contextSentences(text, /работал[аи]?(?:\s+(?:в|как))?|працюва|worked\s+(?:at|as)|previous\s+(?:role|company)|ilgari\s+ish|бұрын\s+жұмыс/iu)].filter(Boolean).join('\n');
|
|
318
|
+
const desired = firstProfession(desiredText);
|
|
319
|
+
const current = firstProfession(currentText);
|
|
320
|
+
const previous = matchProfessions(previousText, { limit: 10, allowWeak: true });
|
|
321
|
+
const reserved = new Set([desired?.id, current?.id].filter(Boolean));
|
|
322
|
+
return deepFreeze({
|
|
323
|
+
desiredProfession: desired,
|
|
324
|
+
currentProfession: current,
|
|
325
|
+
previousProfessions: uniqueProfessionMatches(previous).filter((item) => !reserved.has(item.id)),
|
|
326
|
+
mentionedProfessions: uniqueProfessionMatches(allMatches).filter((item) => !reserved.has(item.id) && !previous.some((prev) => prev.id === item.id)),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const primaryText = [sections.vacancy, title].filter(Boolean).join('\n') || text.slice(0, 500);
|
|
331
|
+
const primary = firstProfession(primaryText) || firstProfession(text.slice(0, 800));
|
|
332
|
+
const mentioned = uniqueProfessionMatches(allMatches).filter((item) => item.id !== primary?.id);
|
|
333
|
+
return deepFreeze({
|
|
334
|
+
vacancyProfession: primary,
|
|
335
|
+
mentionedProfessions: mentioned,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function parseOpeningCount(text) {
|
|
340
|
+
const match = text.match(/(?:требуется|требуются|нужно|набор|ищем|hiring|need|kerak|қажет)[^\d\n]{0,20}(\d{1,3})\s*(?:человек|сотрудник|специалист|people|employees?|xodim|адам)/iu)
|
|
341
|
+
|| text.match(/(\d{1,3})\s*(?:вакансий|позиций|positions?|openings?)/iu);
|
|
342
|
+
return match?.[1] ? Number(match[1]) : null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function parseNoticePeriod(text) {
|
|
346
|
+
const match = text.match(/(?:notice\s+period|срок\s+уведомления|могу\s+выйти\s+через)\s*[:—-]?\s*(\d+)\s*(days?|weeks?|дн(?:я|ей)?|недел[яьи])/iu);
|
|
347
|
+
if (!match?.[1]) return null;
|
|
348
|
+
return deepFreeze({ value: Number(match[1]), unit: /week|недел/i.test(match[2]) ? 'week' : 'day' });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function classifyHiringMessage(value) {
|
|
352
|
+
const text = String(value || '');
|
|
353
|
+
if (!text.trim()) return 'unknown';
|
|
354
|
+
const nonContent = has(text, HIRING_NON_CONTENT_TERMS);
|
|
355
|
+
if (nonContent === 'vacancyDigest') return 'vacancy_digest';
|
|
356
|
+
if (nonContent === 'recruitmentAd') return 'recruitment_ad';
|
|
357
|
+
if (nonContent === 'jobService') return 'job_service';
|
|
358
|
+
if (nonContent === 'spam') return 'spam';
|
|
359
|
+
|
|
360
|
+
const negative = has(text, HIRING_NEGATIVE_INTENT);
|
|
361
|
+
if (negative === 'course') return 'course';
|
|
362
|
+
if (negative === 'closed') return 'closed_vacancy';
|
|
363
|
+
|
|
364
|
+
const candidate = findCanonical(text, [HIRING_INTENT.candidate], { partial: true });
|
|
365
|
+
const employer = findCanonical(text, [HIRING_INTENT.employer], { partial: true });
|
|
366
|
+
if (employer && !candidate) return 'vacancy';
|
|
367
|
+
if (candidate && !employer) return 'candidate';
|
|
368
|
+
if (employer && candidate) {
|
|
369
|
+
const employerMatch = findAllCanonical(text, [HIRING_INTENT.employer])[0];
|
|
370
|
+
const candidateMatch = findAllCanonical(text, [HIRING_INTENT.candidate])[0];
|
|
371
|
+
if (employerMatch && candidateMatch) return employerMatch.start <= candidateMatch.start ? 'vacancy' : 'candidate';
|
|
372
|
+
}
|
|
373
|
+
return 'unknown';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function parseHiringContext(value, { title = '', mode = null } = {}) {
|
|
377
|
+
const text = String(value || '');
|
|
378
|
+
const kind = mode || classifyHiringMessage(text);
|
|
379
|
+
const parserMode = kind === 'candidate' ? 'candidate' : 'vacancy';
|
|
380
|
+
const vacancyStatuses = matchCanonicals(text, VACANCY_STATUS_TERMS);
|
|
381
|
+
const vacancyStatus = ['closed', 'filled', 'frozen', 'open'].find((item) => vacancyStatuses.includes(item)) || null;
|
|
382
|
+
const candidateStatuses = matchCanonicals(text, CANDIDATE_STATUS_TERMS);
|
|
383
|
+
|
|
384
|
+
return deepFreeze({
|
|
385
|
+
kind,
|
|
386
|
+
sections: splitHiringSections(text),
|
|
387
|
+
profession: resolveProfessionContext(text, { mode: parserMode, title }),
|
|
388
|
+
languages: parseLanguageContext(text, { mode: parserMode }),
|
|
389
|
+
requirementRelations: matchCanonicals(text, REQUIREMENT_TERMS),
|
|
390
|
+
hasResponsibilities: Boolean(has(text, RESPONSIBILITY_TERMS)),
|
|
391
|
+
hasOfferSection: Boolean(has(text, OFFER_TERMS)),
|
|
392
|
+
contacts: matchCanonicals(text, CONTACT_TERMS),
|
|
393
|
+
application: matchCanonicals(text, APPLICATION_TERMS),
|
|
394
|
+
companyContext: matchCanonicals(text, COMPANY_TERMS),
|
|
395
|
+
locationContext: matchCanonicals(text, LOCATION_CONTEXT_TERMS),
|
|
396
|
+
workAuthorization: matchCanonicals(text, WORK_AUTHORIZATION_TERMS),
|
|
397
|
+
availability: matchCanonicals(text, HIRING_AVAILABILITY_TERMS),
|
|
398
|
+
travel: has(text, TRAVEL_TERMS),
|
|
399
|
+
relocation: has(text, RELOCATION_CONTEXT_TERMS),
|
|
400
|
+
bonuses: matchCanonicals(text, BONUS_TERMS),
|
|
401
|
+
benefits: matchCanonicals(text, Object.values(BENEFIT_TERMS)),
|
|
402
|
+
contracts: matchCanonicals(text, CONTRACT_CONTEXT_TERMS),
|
|
403
|
+
openingCount: parseOpeningCount(text),
|
|
404
|
+
vacancyStatus,
|
|
405
|
+
candidateStatus: ['notLooking', 'availableImmediately', 'activelyLooking', 'openToOffers', 'employed'].find((item) => candidateStatuses.includes(item)) || null,
|
|
406
|
+
candidatePreferences: matchCanonicals(text, CANDIDATE_PREFERENCE_TERMS),
|
|
407
|
+
employmentHistoryContext: matchCanonicals(text, EMPLOYMENT_HISTORY_TERMS),
|
|
408
|
+
educationContext: has(text, EDUCATION_CONTEXT_TERMS),
|
|
409
|
+
driverContext: matchCanonicals(text, DRIVER_CONTEXT_TERMS),
|
|
410
|
+
noticePeriod: parseNoticePeriod(text),
|
|
411
|
+
});
|
|
412
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type ExtendedLanguageContextItem = Readonly<{
|
|
2
|
+
language: string;
|
|
3
|
+
name: string;
|
|
4
|
+
relation: string | null;
|
|
5
|
+
level: string | null;
|
|
6
|
+
cefr: string | null;
|
|
7
|
+
start: number;
|
|
8
|
+
end: number;
|
|
9
|
+
}>;
|
|
10
|
+
|
|
11
|
+
export const LANGUAGE_LEVEL_EXTENSIONS: readonly Readonly<{
|
|
12
|
+
canonical: string;
|
|
13
|
+
aliases: Readonly<Record<string, readonly string[]>>;
|
|
14
|
+
}>[];
|
|
15
|
+
|
|
16
|
+
export function parseExtendedLanguageContext(
|
|
17
|
+
value: unknown,
|
|
18
|
+
options?: { mode?: 'candidate' | 'vacancy' | null },
|
|
19
|
+
): readonly ExtendedLanguageContextItem[];
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { parseLanguageContext } from './hiring-context.js';
|
|
2
|
+
import { LANGUAGE_LEVELS } from './hiring-languages.js';
|
|
3
|
+
import { findAllCanonical } from './normalization.js';
|
|
4
|
+
import { lexiconEntity } from './lexicon-core.js';
|
|
5
|
+
|
|
6
|
+
const group = (canonical, aliases, extra = {}) => lexiconEntity(canonical, aliases, extra);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Source/feed wording and inflected qualitative levels that are intentionally
|
|
10
|
+
* kept out of consuming applications. Canonicals mirror the core language
|
|
11
|
+
* level taxonomy so callers receive one stable vocabulary.
|
|
12
|
+
*/
|
|
13
|
+
export const LANGUAGE_LEVEL_EXTENSIONS = Object.freeze([
|
|
14
|
+
group('basic', {
|
|
15
|
+
ru: ['базовый', 'базовая', 'базовое', 'базового', 'базовой', 'базовом', 'базовым'],
|
|
16
|
+
en: ['basic'],
|
|
17
|
+
uk: ['базовий', 'базова', 'базове', 'базового'],
|
|
18
|
+
uzLatn: ["boshlang'ich", 'boshlang‘ich', 'boshlangʻich'],
|
|
19
|
+
uzCyrl: ['бошланғич'],
|
|
20
|
+
kk: ['бастапқы'],
|
|
21
|
+
}),
|
|
22
|
+
group('intermediate', {
|
|
23
|
+
ru: ['разговорный', 'разговорная', 'разговорное', 'разговорного', 'средний уровень', 'среднего уровня'],
|
|
24
|
+
en: ['intermediate', 'conversational'],
|
|
25
|
+
uk: ['розмовний', 'розмовна', 'середній рівень'],
|
|
26
|
+
uzLatn: ["o'rta daraja", 'o‘rta daraja', 'oʻrta daraja'],
|
|
27
|
+
uzCyrl: ['ўрта даража'],
|
|
28
|
+
kk: ['орта деңгей'],
|
|
29
|
+
}),
|
|
30
|
+
group('fluent', {
|
|
31
|
+
ru: ['свободный', 'свободная', 'свободное', 'свободного', 'свободное владение'],
|
|
32
|
+
en: ['fluent', 'full professional proficiency'],
|
|
33
|
+
uk: ['вільний', 'вільна', 'вільне володіння'],
|
|
34
|
+
uzLatn: ['erkin', 'erkin daraja'],
|
|
35
|
+
uzCyrl: ['эркин'],
|
|
36
|
+
kk: ['еркін'],
|
|
37
|
+
}),
|
|
38
|
+
group('native', {
|
|
39
|
+
ru: ['родной', 'родная', 'родного', 'родным', 'носитель языка'],
|
|
40
|
+
en: ['native', 'native speaker'],
|
|
41
|
+
uk: ['рідна', 'рідний', 'носій мови'],
|
|
42
|
+
uzLatn: ['ona tili'],
|
|
43
|
+
uzCyrl: ['она тили'],
|
|
44
|
+
kk: ['ана тілі'],
|
|
45
|
+
}),
|
|
46
|
+
group('professional', {
|
|
47
|
+
ru: [
|
|
48
|
+
'профессиональный', 'профессиональная', 'профессиональное', 'профессиональные',
|
|
49
|
+
'профессионального', 'профессиональной', 'профессиональном', 'профессиональным',
|
|
50
|
+
'профессиональное владение', 'профессиональный уровень',
|
|
51
|
+
],
|
|
52
|
+
en: ['professional', 'professional proficiency', 'professional working proficiency'],
|
|
53
|
+
uk: ['професійний', 'професійна', 'професійне', 'професійного', 'професійне володіння'],
|
|
54
|
+
ro: ['profesional', 'nivel profesional'],
|
|
55
|
+
uzLatn: ['professional', 'kasbiy daraja'],
|
|
56
|
+
uzCyrl: ['профессионал', 'касбий даража'],
|
|
57
|
+
kk: ['кәсіби', 'кәсіби деңгей'],
|
|
58
|
+
}),
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const ALL_QUALITATIVE_LEVELS = Object.freeze([...LANGUAGE_LEVELS, ...LANGUAGE_LEVEL_EXTENSIONS]);
|
|
62
|
+
|
|
63
|
+
function distanceToItem(match, item) {
|
|
64
|
+
const itemStart = Number(item?.start || 0);
|
|
65
|
+
const itemEnd = Number(item?.end || itemStart);
|
|
66
|
+
if (match.end <= itemStart) return itemStart - match.end;
|
|
67
|
+
if (match.start >= itemEnd) return match.start - itemEnd;
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function cefrMatches(text) {
|
|
72
|
+
const out = [];
|
|
73
|
+
for (const match of text.matchAll(/(?<![\p{L}\p{N}])([ABC][12])(?![\p{L}\p{N}])/giu)) {
|
|
74
|
+
const start = match.index ?? 0;
|
|
75
|
+
out.push(Object.freeze({
|
|
76
|
+
canonical: match[1].toUpperCase(),
|
|
77
|
+
start,
|
|
78
|
+
end: start + match[0].length,
|
|
79
|
+
kind: 'cefr',
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function qualitativeMatches(text) {
|
|
86
|
+
const seen = new Set();
|
|
87
|
+
return findAllCanonical(text, ALL_QUALITATIVE_LEVELS)
|
|
88
|
+
.filter((match) => {
|
|
89
|
+
const key = `${match.start}:${match.end}:${match.canonical}`;
|
|
90
|
+
if (seen.has(key)) return false;
|
|
91
|
+
seen.add(key);
|
|
92
|
+
return true;
|
|
93
|
+
})
|
|
94
|
+
.map((match) => Object.freeze({ ...match, kind: 'level' }));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function nearestLanguageIndex(token, languages) {
|
|
98
|
+
let bestIndex = -1;
|
|
99
|
+
let bestDistance = Infinity;
|
|
100
|
+
for (let index = 0; index < languages.length; index += 1) {
|
|
101
|
+
const distance = distanceToItem(token, languages[index]);
|
|
102
|
+
if (distance < bestDistance) {
|
|
103
|
+
bestIndex = index;
|
|
104
|
+
bestDistance = distance;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return bestDistance <= 45 ? bestIndex : -1;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function bindLevels(text, languages) {
|
|
111
|
+
const bound = new Map();
|
|
112
|
+
const tokens = [...qualitativeMatches(text), ...cefrMatches(text)];
|
|
113
|
+
for (const token of tokens) {
|
|
114
|
+
const index = nearestLanguageIndex(token, languages);
|
|
115
|
+
if (index < 0) continue;
|
|
116
|
+
const item = languages[index];
|
|
117
|
+
const distance = distanceToItem(token, item);
|
|
118
|
+
const current = bound.get(index);
|
|
119
|
+
if (!current || distance < current.distance || (distance === current.distance && token.start < current.token.start)) {
|
|
120
|
+
bound.set(index, { token, distance });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return bound;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function parseExtendedLanguageContext(value, options = {}) {
|
|
127
|
+
const text = String(value || '');
|
|
128
|
+
const parsed = parseLanguageContext(text, options);
|
|
129
|
+
const bound = bindLevels(text, parsed);
|
|
130
|
+
return Object.freeze(parsed.map((item, index) => {
|
|
131
|
+
const token = bound.get(index)?.token;
|
|
132
|
+
if (!token) return item;
|
|
133
|
+
return Object.freeze({
|
|
134
|
+
...item,
|
|
135
|
+
level: token.kind === 'level' ? token.canonical : null,
|
|
136
|
+
cefr: token.kind === 'cefr' ? token.canonical : null,
|
|
137
|
+
});
|
|
138
|
+
}));
|
|
139
|
+
}
|