@link-foundation/sensitive-data-sanitizer 0.18.0 → 0.19.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.
@@ -0,0 +1,246 @@
1
+ import { collectMatches } from './detection.js';
2
+ import { isDocumentTechnology } from './document-experience.js';
3
+
4
+ const b = '[\\p{L}\\p{N}_]';
5
+ const proper = '[\\p{Lu}][\\p{L}\\p{M}.-]{1,47}';
6
+ const rules = [
7
+ [
8
+ 'PHONE',
9
+ 'document-local-phone',
10
+ /(?<![\p{L}\p{N}_])(?:тел\.?|телефон|phone)[ \t]*:?[ \t]*(\d{7})(?!\d)/giu,
11
+ 1,
12
+ ],
13
+ [
14
+ 'PHONE',
15
+ 'document-spaced-mobile',
16
+ /(?<![\p{L}\p{N}_])8[ \t]+9(?:[ \t]+\d){9}(?!\d)/gu,
17
+ 0,
18
+ ],
19
+ [
20
+ 'EMAIL',
21
+ 'document-email-obfuscation',
22
+ /(?<![\p{L}\p{N}_])[\p{L}\p{N}_.+-]{1,64}\(собака\)[\p{L}\p{N}-]{1,63}(?:\(точка\)[\p{L}\p{N}-]{1,63}){1,6}(?![\p{L}\p{N}_])/giu,
23
+ 0,
24
+ ],
25
+ [
26
+ 'EMAIL',
27
+ 'document-email-spaces',
28
+ /(?<![\p{L}\p{N}_])(?:[a-z][ \t]+){1,63}[a-z][ \t]*@[ \t]*[a-z\d-]{1,63}[ \t]*\.[ \t]*[a-z]{2,20}(?![\p{L}\p{N}_])/giu,
29
+ 0,
30
+ ],
31
+ [
32
+ 'USERNAME',
33
+ 'document-messenger',
34
+ /(?<![\p{L}\p{N}_])(?:discord|max)[ \t]*:[ \t]*([a-z][a-z\d_.-]{1,63}(?:#\d{4})?)(?![\p{L}\p{N}_])/giu,
35
+ 1,
36
+ ],
37
+ [
38
+ 'URL',
39
+ 'document-social-link',
40
+ /(?<![\p{L}\p{N}_./-])(?:https?:\/\/)?(?:www\.)?(?:ok\.ru\/profile|gitlab\.com|habr\.com\/ru\/users|facebook\.com)\/[\p{L}\p{N}_.-]{2,100}(?:\/)?(?:[?#][^\s"<>\\]*)?/giu,
41
+ 0,
42
+ ],
43
+ [
44
+ 'URL',
45
+ 'document-resume-link',
46
+ /https?:\/\/[^\s/"<>\\]+\/(?:cv\.pdf(?:[?#][^\s"<>\\]*)?|cv(?:\/[^\s"<>\\]*)?|~[\p{L}\p{N}_.-]{1,100}\/?)/giu,
47
+ 0,
48
+ ],
49
+ [
50
+ 'DATE_OF_BIRTH',
51
+ 'document-birthday',
52
+ /(?<![\p{L}\p{N}_])(?:д\.р\.|день рождения)[ \t]*:?[ \t]*(\d{1,2}(?:[/.]\d{1,2}[/.]\d{2,4}|[ \t]+(?:января|февраля|марта|апреля|мая|июня|июля|августа|сентября|октября|ноября|декабря)(?:[ \t]+\d{4})?))(?!\d)/giu,
53
+ 1,
54
+ ],
55
+ [
56
+ 'AGE',
57
+ 'document-age-unit',
58
+ /(?<![\p{L}\p{N}_])\d{1,3}[ \t]*(?:y\.o\.|лет[ \t]+от[ \t]+роду)(?![\p{L}\p{N}_])/giu,
59
+ 0,
60
+ ],
61
+ [
62
+ 'ID',
63
+ 'document-military-id',
64
+ /(?<![\p{L}\p{N}_])военный билет[ \t]+([А-ЯЁ]{2}[ \t]+\d{7})(?!\d)/giu,
65
+ 1,
66
+ ],
67
+ [
68
+ 'VIN',
69
+ 'document-vin',
70
+ /(?<![\p{L}\p{N}_])VIN[ \t]*:?[ \t]*([A-HJ-NPR-Z\d]{17})(?![\p{L}\p{N}_])/giu,
71
+ 1,
72
+ ],
73
+ [
74
+ 'ORGANIZATION',
75
+ 'document-legal-lowercase',
76
+ /(?<![\p{L}\p{N}_])(?:ооо|зао|пао|ао)[ \t]+[а-яё][а-яё-]{1,47}(?![\p{L}\p{N}_])/gu,
77
+ 0,
78
+ ],
79
+ [
80
+ 'ORGANIZATION',
81
+ 'document-employer-dash',
82
+ new RegExp(
83
+ `(?<!${b})(?:Компания|Место работы)[ \\t]+[-–—][ \\t]+(${proper})(?!${b})`,
84
+ 'gu'
85
+ ),
86
+ 1,
87
+ ],
88
+ [
89
+ 'ORGANIZATION',
90
+ 'document-employer-prefix',
91
+ new RegExp(`(?<!${b})(?:Группа|стартап)[ \\t]+(${proper})(?!${b})`, 'gu'),
92
+ 1,
93
+ ],
94
+ [
95
+ 'ORGANIZATION',
96
+ 'document-bank-suffix',
97
+ new RegExp(`(?<!${b})${proper}[ \\t]+Банк(?!${b})`, 'gu'),
98
+ 0,
99
+ ],
100
+ [
101
+ 'ORGANIZATION',
102
+ 'document-industry',
103
+ new RegExp(
104
+ `(?<!${b})(${proper})(?=[ \\t]*\\((?:ритейл|retail|финансы|finance|IT)\\))`,
105
+ 'gu'
106
+ ),
107
+ 1,
108
+ ],
109
+ [
110
+ 'ORGANIZATION',
111
+ 'document-reversed-role',
112
+ new RegExp(
113
+ `(?<!${b})(${proper})[ \\t]*(?:,|[-–—])[ \\t]*(?:менеджер|manager|директор|developer|инженер)(?!${b})`,
114
+ 'gu'
115
+ ),
116
+ 1,
117
+ ],
118
+ [
119
+ 'ORGANIZATION',
120
+ 'document-role-employer',
121
+ new RegExp(
122
+ `(?<!${b})(?:менеджер|директор|инженер)[ \\t]+(?:в[ \\t]+)?(${proper})(?!${b})`,
123
+ 'gu'
124
+ ),
125
+ 1,
126
+ ],
127
+ [
128
+ 'ORGANIZATION',
129
+ 'document-at-employer',
130
+ /(?<![\p{L}\p{N}_])at[ \t]+([A-Z][a-z][\p{L}.-]{0,45})(?![\p{L}\p{N}_])/gu,
131
+ 1,
132
+ ],
133
+ [
134
+ 'EDUCATION',
135
+ 'document-school-context',
136
+ /(?<![\p{L}\p{N}_])(?:graduated from|бакалавр,?|диплом|колледж|[Кк]урсы|курс на)[ \t]+([\p{Lu}][\p{L}.-]{1,47})(?![\p{L}\p{N}_])/gu,
137
+ 1,
138
+ ],
139
+ [
140
+ 'EDUCATION',
141
+ 'document-school-year',
142
+ /(?<![\p{L}\p{N}_])[А-ЯЁA-Z]{2,12}(?=,[ \t]*(?:19|20)\d{2}(?!\d))/gu,
143
+ 0,
144
+ ],
145
+ [
146
+ 'EDUCATION',
147
+ 'document-msu',
148
+ /(?<![\p{L}\p{N}_])МГУ(?:[ \t]+им\.[ \t]+[А-ЯЁ][а-яё-]{1,40})?(?![\p{L}\p{N}_])/gu,
149
+ 0,
150
+ ],
151
+ [
152
+ 'PERSON',
153
+ 'document-patronymic',
154
+ /(?<![\p{L}\p{N}_])[А-ЯЁ][а-яё]{2,32}(?:овна|евна|ична|ович|евич)(?![\p{L}\p{N}_])/gu,
155
+ 0,
156
+ ],
157
+ [
158
+ 'PERSON',
159
+ 'document-uppercase-initials',
160
+ /(?<![\p{L}\p{N}_])[А-ЯЁ]{2,40}[ \t]+(?:[А-ЯЁA-Z]\.[ \t]*){1,2}(?![\p{L}\p{N}_])/gu,
161
+ 0,
162
+ ],
163
+ [
164
+ 'PERSON',
165
+ 'document-unfamiliar-name',
166
+ /(?<![\p{L}\p{N}_])(?:[А-ЯЁ][а-яё]{1,30}[ \t]+(?:Ким|[А-ЯЁ][а-яё]{1,25}(?:чук|енко|швили|дзе))|Ли[ \t]+[А-ЯЁ][а-яё]{1,20})(?![\p{L}\p{N}_])/gu,
167
+ 0,
168
+ ],
169
+ [
170
+ 'ADDRESS',
171
+ 'document-street-complete',
172
+ /(?<![\p{L}\p{N}_])(?:(?:ул\.?|улица|пр-т)[ \t]+[А-ЯЁ][а-яё-]{1,40},?[ \t]+(?:д\.[ \t]*)?\d{1,5}(?:-\d{1,5})?|[А-ЯЁ][а-яё-]{2,40}[ \t]+шоссе,[ \t]*\d{1,5})(?:,[ \t]*кв\.?[ \t]*\d{1,5})?(?!\d)/gu,
173
+ 0,
174
+ ],
175
+ [
176
+ 'LOCATION',
177
+ 'document-region',
178
+ /(?<![\p{L}\p{N}_])[А-ЯЁ][а-яё-]{2,40}[ \t]+(?:область|район)(?![\p{L}\p{N}_])/gu,
179
+ 0,
180
+ ],
181
+ [
182
+ 'PERSONAL_ATTRIBUTE',
183
+ 'document-citizenship',
184
+ /(?<![\p{L}\p{N}_])(?:гражданство|гражданин|гражданка)[ \t]+([А-ЯЁ][А-ЯЁа-яё-]{1,40})(?![\p{L}\p{N}_])/gu,
185
+ 1,
186
+ ],
187
+ [
188
+ 'LICENSE_PLATE',
189
+ 'document-vehicle-plate',
190
+ /(?<![\p{L}\p{N}_])[АВЕКМНОРСТУХABEKMHOPCTYX]\d{3}[АВЕКМНОРСТУХABEKMHOPCTYX]{2}[ \t]*\d{2,3}(?:[ \t]+RUS)?(?![\p{L}\p{N}_])/gu,
191
+ 0,
192
+ ],
193
+ ];
194
+
195
+ export function detectDocumentCatalog(text, emit) {
196
+ for (const [type, rule, pattern, group] of rules) {
197
+ collectMatches(
198
+ text,
199
+ pattern,
200
+ emit,
201
+ { type, rule, category: 'personal', confidence: 0.85 },
202
+ group,
203
+ (value) => type !== 'ORGANIZATION' || !isDocumentTechnology(value)
204
+ );
205
+ }
206
+ // Lookalike substitution preserves UTF-16 offsets and is limited to words
207
+ // containing Cyrillic plus Latin letters. Ordinary English is untouched.
208
+ const lookalikes = {
209
+ A: 'А',
210
+ a: 'а',
211
+ B: 'В',
212
+ C: 'С',
213
+ c: 'с',
214
+ E: 'Е',
215
+ e: 'е',
216
+ H: 'Н',
217
+ K: 'К',
218
+ M: 'М',
219
+ O: 'О',
220
+ o: 'о',
221
+ P: 'Р',
222
+ p: 'р',
223
+ T: 'Т',
224
+ X: 'Х',
225
+ x: 'х',
226
+ y: 'у',
227
+ };
228
+ const folded = text.replace(/[\p{L}]+/gu, (word) =>
229
+ /[А-Яа-яЁё]/u.test(word)
230
+ ? word.replace(/[AaBCcEeHKMOoPpTXxy]/g, (c) => lookalikes[c])
231
+ : word
232
+ );
233
+ if (folded !== text) {
234
+ collectMatches(
235
+ folded,
236
+ /(?<![\p{L}\p{N}_])[А-ЯЁ][а-яё]{1,30}[ \t]+[А-ЯЁ][а-яё]{1,28}(?:ова|ева|ина|ов|ев|ин|ский|ская)(?![\p{L}\p{N}_])/gu,
237
+ emit,
238
+ {
239
+ type: 'PERSON',
240
+ rule: 'document-mixed-name',
241
+ category: 'personal',
242
+ confidence: 0.9,
243
+ }
244
+ );
245
+ }
246
+ }
@@ -6,6 +6,8 @@ import { documentDateRange as range } from './document-dates.js';
6
6
  import { detectDocumentExtras } from './document-extras.js';
7
7
  import { detectDocumentPatterns } from './document-patterns.js';
8
8
  import { isDocumentTechnology } from './document-experience.js';
9
+ import { detectVocabulary } from './vocabulary.js';
10
+ import { detectDocumentCatalog } from './document-catalog.js';
9
11
 
10
12
  const words = (column) =>
11
13
  [...new Set(documentVocabulary.flatMap((row) => row[column].split('|')))]
@@ -79,8 +81,10 @@ export function detectDocumentEntities(text, emit, options) {
79
81
  if (!options.documentEntities) {
80
82
  return;
81
83
  }
84
+ detectVocabulary(text, emit, options);
85
+ detectDocumentCatalog(text, emit);
82
86
  detectDocumentPatterns(text, emit);
83
- detectDocumentExtras(text, emit, range, options.debug);
87
+ detectDocumentExtras(text, emit, range, options.debug, options);
84
88
  collectMatches(
85
89
  text,
86
90
  new RegExp(
@@ -120,7 +124,9 @@ export function detectDocumentEntities(text, emit, options) {
120
124
  capture,
121
125
  { type, rule, category: 'personal', confidence },
122
126
  group,
123
- (value) => type !== 'ORGANIZATION' || !isDocumentTechnology(value)
127
+ (value) =>
128
+ type !== 'ORGANIZATION' ||
129
+ (!isDocumentTechnology(value) && value !== 'ОГРНИП')
124
130
  );
125
131
  }
126
132
  for (const match of text.matchAll(ruResidence)) {
@@ -1,3 +1,5 @@
1
+ import { compiledVocabulary } from './vocabulary.js';
2
+
1
3
  const technology = new Set(
2
4
  [
3
5
  'Python',
@@ -45,7 +47,7 @@ function commaParts(text) {
45
47
  return parts;
46
48
  }
47
49
 
48
- function employerSpan(prefix) {
50
+ function employerSpan(prefix, roles) {
49
51
  // An industry suffix belongs to the entry, but is not an employer. A pipe
50
52
  // before the title remains a section separator, as in `Опыт | Менеджер, ...`.
51
53
  const comma = prefix.indexOf(',');
@@ -59,7 +61,7 @@ function employerSpan(prefix) {
59
61
  if (/^[=\s\d]/u.test(parts[0].text)) {
60
62
  index++;
61
63
  }
62
- if (parts.length === 2 && reversedRole.test(parts[1].text)) {
64
+ if (parts.length === 2 && roles.test(parts[1].text)) {
63
65
  index = 0;
64
66
  } else {
65
67
  while (index < parts.length - 1 && /^\p{Ll}/u.test(parts[index].text)) {
@@ -77,7 +79,7 @@ function employerSpan(prefix) {
77
79
  : { start, end };
78
80
  }
79
81
 
80
- export function detectDatedEmployers(text, emit, range, debug) {
82
+ export function detectDatedEmployers(text, emit, range, debug, options = {}) {
81
83
  let cursor = 0,
82
84
  count = 0;
83
85
  // Match only the anchor, then visit each preceding segment once. No lazy
@@ -91,7 +93,10 @@ export function detectDatedEmployers(text, emit, range, debug) {
91
93
  segment.lastIndexOf(';')
92
94
  ) + 1;
93
95
  const prefix = segment.slice(boundary).replace(/^[., \t]+/u, '');
94
- const span = employerSpan(prefix);
96
+ const span = employerSpan(
97
+ prefix,
98
+ compiledVocabulary(options.vocabulary)?.roles ?? reversedRole
99
+ );
95
100
  if (span) {
96
101
  const offset = match.index - prefix.length;
97
102
  emit({
@@ -18,7 +18,7 @@ const duration = new RegExp(
18
18
  `(?<!${boundary})(${name})(?=[ \\t]*\\(\\d{1,2}[ \\t]+(?:год|года|лет|years?)\\))`,
19
19
  'gu'
20
20
  );
21
- export function detectDocumentExtras(text, emit, range, debug) {
21
+ export function detectDocumentExtras(text, emit, range, debug, options) {
22
22
  for (const [type, rule, pattern, group] of [
23
23
  ['PHONE', 'document-mobile-bare', /^[78]9\d{9}$/gm, 0],
24
24
  [
@@ -128,7 +128,7 @@ export function detectDocumentExtras(text, emit, range, debug) {
128
128
  isDocumentShareLink
129
129
  );
130
130
  detectEmployerContinuation(text, emit);
131
- detectDatedEmployers(text, emit, range, debug);
131
+ detectDatedEmployers(text, emit, range, debug, options);
132
132
  }
133
133
 
134
134
  function detectEmployerContinuation(text, emit) {
@@ -1,71 +1,83 @@
1
1
  // Authored, deliberately finite vocabulary; no network or model downloads.
2
2
  // Each row follows localeVocabulary: locale, legal forms, school types, places.
3
- export const documentVocabulary = [
3
+ export const documentVocabulary = Object.freeze(
4
4
  [
5
- 'en',
6
- 'Inc|LLC|Ltd|PLC|Corporation',
7
- 'University|College|Institute|School',
8
- 'Springfield|Riverton|London|New York|California',
9
- ],
10
- [
11
- 'es',
12
- 'S.A.|S.L.|S.L.U.',
13
- 'Universidad|Colegio|Instituto|Escuela',
14
- 'Madrid|Barcelona|Andalucía',
15
- ],
16
- [
17
- 'fr',
18
- 'SARL|SAS|SA|S.A.',
19
- 'Université|Collège|Institut|École',
20
- 'Paris|Lyon|Bretagne',
21
- ],
22
- [
23
- 'de',
24
- 'GmbH|AG|KG|UG',
25
- 'Universität|Hochschule|Institut|Schule',
26
- 'Berlin|München|Bayern',
27
- ],
28
- [
29
- 'pt',
30
- 'Ltda|S.A.|Unipessoal',
31
- 'Universidade|Faculdade|Instituto|Escola',
32
- 'Lisboa|São Paulo|Algarve',
33
- ],
34
- [
35
- 'ru',
36
- 'ООО|АО|ПАО|ИП|ЗАО',
37
- 'Университет|университет|Институт|институт|Школа|школа',
38
- 'Москва|Санкт-Петербург|Казань|Татарстан|Ярославль|Кострома|Новосибирск|Екатеринбург|Нижний Новгород|Самара|Омск|Челябинск|Ростов-на-Дону|Уфа|Красноярск|Пермь|Воронеж|Волгоград|Саратов|Тюмень|Тольятти|Ижевск|Барнаул|Иркутск|Хабаровск|Владивосток|Махачкала|Томск|Оренбург|Кемерово|Рязань|Астрахань|Пенза|Липецк|Киров|Чебоксары|Тула|Калининград|Курск|Улан-Удэ|Ставрополь|Сочи|Тверь|Брянск|Белгород|Владимир|Архангельск|Калуга|Смоленск|Вологда|Саранск|Орёл',
39
- ],
40
- ['ja', '株式会社|有限会社|合同会社', '大学|学院|学校', '東京|大阪|北海道'],
41
- ['zh', '有限公司|股份有限公司', '大学|学院|学校', '北京|上海|广东'],
42
- ['zh-Hant', '有限公司|股份有限公司', '大學|學院|學校', '臺北|台北|高雄|臺灣'],
43
- ['ko', '주식회사|유한회사', '대학교|대학|학교', '서울|부산|경기도'],
44
- ['ar', 'ذ.م.م|شركة', 'جامعة|معهد|مدرسة', 'القاهرة|دبي|الرياض'],
45
- [
46
- 'he',
47
- 'בע״מ|בע"מ|חברה',
48
- 'אוניברסיטת|אוניברסיטה|מכללת|מכון',
49
- 'ירושלים|תל אביב|חיפה',
50
- ],
51
- [
52
- 'hi',
53
- 'प्राइवेट लिमिटेड|लिमिटेड',
54
- 'विश्वविद्यालय|महाविद्यालय|संस्थान',
55
- 'दिल्ली|मुंबई|महाराष्ट्र',
56
- ],
57
- [
58
- 'tr',
59
- 'A.Ş.|Ltd. Şti.',
60
- 'Üniversitesi|Üniversite|Enstitüsü|Okulu',
61
- 'İstanbul|Ankara|İzmir',
62
- ],
63
- ['id', 'PT|CV|Tbk', 'Universitas|Institut|Sekolah', 'Jakarta|Bandung|Bali'],
64
- ['vi', 'TNHH|CTCP', 'Đại học|Trường|Học viện', 'Hà Nội|Hồ Chí Minh|Đà Nẵng'],
65
- [
66
- 'th',
67
- 'บริษัท|จำกัด',
68
- 'มหาวิทยาลัย|วิทยาลัย|โรงเรียน',
69
- 'กรุงเทพมหานคร|เชียงใหม่|ภูเก็ต',
70
- ],
71
- ];
5
+ [
6
+ 'en',
7
+ 'Inc|LLC|Ltd|PLC|Corporation',
8
+ 'University|College|Institute|School',
9
+ 'Springfield|Riverton|London|New York|California',
10
+ ],
11
+ [
12
+ 'es',
13
+ 'S.A.|S.L.|S.L.U.',
14
+ 'Universidad|Colegio|Instituto|Escuela',
15
+ 'Madrid|Barcelona|Andalucía',
16
+ ],
17
+ [
18
+ 'fr',
19
+ 'SARL|SAS|SA|S.A.',
20
+ 'Université|Collège|Institut|École',
21
+ 'Paris|Lyon|Bretagne',
22
+ ],
23
+ [
24
+ 'de',
25
+ 'GmbH|AG|KG|UG',
26
+ 'Universität|Hochschule|Institut|Schule',
27
+ 'Berlin|München|Bayern',
28
+ ],
29
+ [
30
+ 'pt',
31
+ 'Ltda|S.A.|Unipessoal',
32
+ 'Universidade|Faculdade|Instituto|Escola',
33
+ 'Lisboa|São Paulo|Algarve',
34
+ ],
35
+ [
36
+ 'ru',
37
+ 'ООО|АО|ПАО|ИП|ЗАО',
38
+ 'Университет|университет|Институт|институт|Школа|школа',
39
+ 'Москва|Санкт-Петербург|Казань|Татарстан|Ярославль|Кострома|Новосибирск|Екатеринбург|Нижний Новгород|Самара|Омск|Челябинск|Ростов-на-Дону|Уфа|Красноярск|Пермь|Воронеж|Волгоград|Саратов|Тюмень|Тольятти|Ижевск|Барнаул|Иркутск|Хабаровск|Владивосток|Махачкала|Томск|Оренбург|Кемерово|Рязань|Астрахань|Пенза|Липецк|Киров|Чебоксары|Тула|Калининград|Курск|Улан-Удэ|Ставрополь|Сочи|Тверь|Брянск|Белгород|Владимир|Архангельск|Калуга|Смоленск|Вологда|Саранск|Орёл',
40
+ ],
41
+ ['ja', '株式会社|有限会社|合同会社', '大学|学院|学校', '東京|大阪|北海道'],
42
+ ['zh', '有限公司|股份有限公司', '大学|学院|学校', '北京|上海|广东'],
43
+ [
44
+ 'zh-Hant',
45
+ '有限公司|股份有限公司',
46
+ '大學|學院|學校',
47
+ '臺北|台北|高雄|臺灣',
48
+ ],
49
+ ['ko', '주식회사|유한회사', '대학교|대학|학교', '서울|부산|경기도'],
50
+ ['ar', 'ذ.م.م|شركة', 'جامعة|معهد|مدرسة', 'القاهرة|دبي|الرياض'],
51
+ [
52
+ 'he',
53
+ 'בע״מ|בע"מ|חברה',
54
+ 'אוניברסיטת|אוניברסיטה|מכללת|מכון',
55
+ 'ירושלים|תל אביב|חיפה',
56
+ ],
57
+ [
58
+ 'hi',
59
+ 'प्राइवेट लिमिटेड|लिमिटेड',
60
+ 'विश्वविद्यालय|महाविद्यालय|संस्थान',
61
+ 'दिल्ली|मुंबई|महाराष्ट्र',
62
+ ],
63
+ [
64
+ 'tr',
65
+ 'A.Ş.|Ltd. Şti.',
66
+ 'Üniversitesi|Üniversite|Enstitüsü|Okulu',
67
+ 'İstanbul|Ankara|İzmir',
68
+ ],
69
+ ['id', 'PT|CV|Tbk', 'Universitas|Institut|Sekolah', 'Jakarta|Bandung|Bali'],
70
+ [
71
+ 'vi',
72
+ 'TNHH|CTCP',
73
+ 'Đại học|Trường|Học viện',
74
+ 'Hà Nội|Hồ Chí Minh|Đà Nẵng',
75
+ ],
76
+ [
77
+ 'th',
78
+ 'บริษัท|จำกัด',
79
+ 'มหาวิทยาลัย|วิทยาลัย|โรงเรียน',
80
+ 'กรุงเทพมหานคร|เชียงใหม่|ภูเก็ต',
81
+ ],
82
+ ].map((row) => Object.freeze(row))
83
+ );