euredact 0.5.1 → 0.6.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/dist/cjs/sdk.js CHANGED
@@ -86,6 +86,96 @@ function applyReplacements(text, detections, labelFor) {
86
86
  parts.push(text.slice(pos));
87
87
  return parts.join("");
88
88
  }
89
+ /**
90
+ * Bracket-mask `text` and record where each label landed.
91
+ *
92
+ * Returns what is sent to the cloud tier, plus one `WireLabel` per label.
93
+ * Everything between labels is copied unchanged, so the labels are all it
94
+ * takes to carry an offset from the masked text back to the original.
95
+ *
96
+ * Always `[TYPE]`, whatever label scheme the caller asked for. That is the
97
+ * syntax the model was trained on, where a placeholder means "already handled,
98
+ * do not list it". A token or a numbered label would be read as ordinary text,
99
+ * so the format on the wire is a model contract and not an output option.
100
+ */
101
+ function maskForCloud(text, detections) {
102
+ const parts = [];
103
+ const labels = [];
104
+ let pos = 0;
105
+ let out = 0;
106
+ for (const det of detections) {
107
+ const start = Math.max(det.start, pos);
108
+ if (det.end <= start)
109
+ continue;
110
+ const label = `[${det.entityType}]`;
111
+ out += start - pos;
112
+ parts.push(text.slice(pos, start), label);
113
+ labels.push([out, out + label.length, start, det.end]);
114
+ out += label.length;
115
+ pos = det.end;
116
+ }
117
+ parts.push(text.slice(pos));
118
+ return [parts.join(""), labels];
119
+ }
120
+ /** Index of the last label starting at or before `offset` — strictly before
121
+ * when `strict` — or -1. */
122
+ function labelBefore(labels, offset, strict) {
123
+ let lo = 0;
124
+ let hi = labels.length;
125
+ while (lo < hi) {
126
+ const mid = (lo + hi) >>> 1;
127
+ if (strict ? labels[mid][0] < offset : labels[mid][0] <= offset)
128
+ lo = mid + 1;
129
+ else
130
+ hi = mid;
131
+ }
132
+ return lo - 1;
133
+ }
134
+ /**
135
+ * Carry the service's spans from the masked text onto the original.
136
+ *
137
+ * The service only ever saw `masked`, so its offsets index that. An offset in
138
+ * copied text shifts by whatever the labels before it added or removed. An
139
+ * offset inside a label has no counterpart in the original, so it snaps
140
+ * outward to the edge of the value the label replaced: an address the model
141
+ * reports around `[POSTAL_CODE]` covers the postal code, never half of it.
142
+ * Over-masking is the safe direction.
143
+ *
144
+ * A span lying wholly inside one label describes the label, not the document.
145
+ * It is dropped; the local detection behind the label stands.
146
+ *
147
+ * Throws `Unplaceable` when a span does not match the text that was sent. Such
148
+ * a span cannot be placed, and masking where it points would cover the wrong
149
+ * characters and leave the right ones in the clear.
150
+ */
151
+ function ontoOriginal(spans, masked, text, labels, Unplaceable) {
152
+ const placed = [];
153
+ for (const span of spans) {
154
+ if (!(0 <= span.start && span.start <= span.end && span.end <= masked.length) ||
155
+ masked.slice(span.start, span.end) !== span.text) {
156
+ throw new Unplaceable("span offsets do not match the text that was sent; cannot place the " +
157
+ "service's detections in the document");
158
+ }
159
+ if (span.start === span.end)
160
+ continue;
161
+ let start = span.start;
162
+ let at = labelBefore(labels, span.start, false);
163
+ if (at >= 0) {
164
+ const [, maskedEnd, origStart, origEnd] = labels[at];
165
+ if (span.end <= maskedEnd)
166
+ continue;
167
+ start = span.start < maskedEnd ? origStart : origEnd + span.start - maskedEnd;
168
+ }
169
+ let end = span.end;
170
+ at = labelBefore(labels, span.end, true);
171
+ if (at >= 0) {
172
+ const [, maskedEnd, , origEnd] = labels[at];
173
+ end = origEnd + Math.max(0, span.end - maskedEnd);
174
+ }
175
+ placed.push({ ...span, start, end, text: text.slice(start, end) });
176
+ }
177
+ return placed;
178
+ }
89
179
  /**
90
180
  * Characters a token suffix is drawn from. No vowels, so a suffix never spells
91
181
  * a word; no 0/1/I/O, so it survives being read back by a person; no
@@ -425,7 +515,16 @@ class EuRedact {
425
515
  return (det) => `[${det.entityType}]`;
426
516
  }
427
517
  /**
428
- * Send a document to the cloud tier.
518
+ * Run the rules here, then send what they left to the cloud tier.
519
+ *
520
+ * Local-first: the rules engine masks everything it can find on the caller's
521
+ * machine, and only that masked text is sent. The service looks for what has
522
+ * no shape to match on — names, employers, diagnoses — and answers with
523
+ * spans relative to the text it received, which are mapped back onto the
524
+ * original here.
525
+ *
526
+ * This is minimisation, not an exemption: what the rules miss, and the names
527
+ * and diagnoses the model is there to find, still travel.
429
528
  *
430
529
  * Options the service cannot honour throw rather than being ignored. Silently
431
530
  * dropping one would mean returning a result that does not match what was
@@ -434,6 +533,7 @@ class EuRedact {
434
533
  */
435
534
  async redactViaCloud(text, options) {
436
535
  const { CloudClient } = await Promise.resolve().then(() => __importStar(require("./cloud/client.js")));
536
+ const { CloudError } = await Promise.resolve().then(() => __importStar(require("./cloud/errors.js")));
437
537
  const countries = options.countries ?? null;
438
538
  if (!countries || countries.length !== 1) {
439
539
  throw new Error('cloud mode needs exactly one country, e.g. { countries: ["BE"] }. The ' +
@@ -451,43 +551,34 @@ class EuRedact {
451
551
  if (options.referentialIntegrity) {
452
552
  throw new Error("referentialIntegrity is not supported in cloud mode");
453
553
  }
454
- // detectDates is deliberately NOT forwarded. The service always runs its
455
- // rules engine with dates on, because that is what the model was trained
456
- // against; the caller's value cannot change that. It is the one ignored
457
- // option that is safe to ignore — it can only cause MORE to be detected,
458
- // never less, so it cannot produce under-redaction.
459
554
  const allowed = this.allowedFor(options);
460
555
  const domains = this.domainsFor(options);
461
- const result = await new CloudClient().redact(text, { country: countries[0] });
462
- if (options.tokenize || allowed.size > 0 || domains.size > 0) {
463
- await this.remaskCloudResult(result, text, { tokenize: options.tokenize ?? false, allowed, domains });
464
- }
465
- return result;
466
- }
467
- /**
468
- * Rebuild the masked text from the service's spans, in place.
469
- *
470
- * The service masks with bracketed labels only. Any other output option is
471
- * applied here, from its spans: the service builds its `redacted_text` from
472
- * exactly the spans it returns (entities it reports but cannot place are
473
- * listed separately and never applied), so nothing it masked is lost by
474
- * masking again from the same spans.
475
- *
476
- * The service reports offsets in code points and this SDK slices UTF-16
477
- * units, so the slice check is what stands between an emoji in the document
478
- * and a rebuilt text with the wrong characters masked.
479
- */
480
- async remaskCloudResult(result, text, opts) {
481
- const { CloudError } = await Promise.resolve().then(() => __importStar(require("./cloud/errors.js")));
482
- for (const det of result.detections) {
483
- if (text.slice(det.start, det.end) !== det.text) {
484
- throw new CloudError("span offsets do not match the document; cannot apply tokenize/allowlist locally");
485
- }
486
- }
487
- [result.detections, result.exempted] = applyAllowlist(text, result.detections, opts.allowed, opts.domains);
488
- const tokenMapper = opts.tokenize ? new TokenMapper(text, result.detections) : null;
489
- result.redactedText = applyReplacements(text, result.detections, this.labelFor(false, tokenMapper));
490
- result.tokens = tokenMapper ? tokenMapper.tokens : {};
556
+ const client = new CloudClient();
557
+ // Dates are always on, whatever detectDates says: that is the rules output
558
+ // the model was trained against, and a date of birth the rules can place
559
+ // has no reason to travel. It is the one ignored option that is safe to
560
+ // ignore — it can only cause MORE to be detected, never less.
561
+ //
562
+ // No allowlist and no tokens on this pass. An exemption says what the
563
+ // caller wants back, not what may leave; both are applied below, after the
564
+ // response, to the merged spans.
565
+ const local = this.redactRules(text, { countries, detectDates: true, cache: options.cache }, new Map(), new Map());
566
+ const [masked, labels] = maskForCloud(text, local.detections);
567
+ // The client converts the service's code-point offsets to UTF-16 units of
568
+ // the text it sent, so everything from here on is in one unit.
569
+ const remote = await client.redact(masked, { country: countries[0] });
570
+ // A new array throughout: `local` may be the cached rules result.
571
+ const [detections, exempted] = applyAllowlist(text, [...local.detections, ...ontoOriginal(remote.detections, masked, text, labels, CloudError)], allowed, domains);
572
+ detections.sort((a, b) => a.start - b.start || b.end - a.end);
573
+ const tokenMapper = options.tokenize ? new TokenMapper(text, detections) : null;
574
+ return {
575
+ ...local,
576
+ redactedText: applyReplacements(text, detections, this.labelFor(false, tokenMapper)),
577
+ detections,
578
+ source: "cloud",
579
+ tokens: tokenMapper ? tokenMapper.tokens : {},
580
+ exempted,
581
+ };
491
582
  }
492
583
  redact(text, options = {}) {
493
584
  checkCountryArg(options.countries, "countries");
@@ -508,12 +599,16 @@ class EuRedact {
508
599
  if (requestedMode !== "rules") {
509
600
  throw new Error(`unknown mode ${JSON.stringify(requestedMode)}: expected "rules" or "cloud"`);
510
601
  }
602
+ return this.redactRules(text, options, allowed, domains);
603
+ }
604
+ /** The local pipeline: normalise, detect, exempt, mask. */
605
+ redactRules(text, options, allowed, domains) {
511
606
  if (text.length > this.maxInputLength) {
512
607
  throw new Error(`Input text length (${text.length.toLocaleString()} chars) exceeds the maximum ` +
513
608
  `(${this.maxInputLength.toLocaleString()} chars). Split the input or ` +
514
609
  `increase maxInputLength when constructing EuRedact.`);
515
610
  }
516
- const { countries = null, countryHint = null, context = null, chunkOffset = 0, mode = "rules", referentialIntegrity = false, tokenize = false, detectDates = false, } = options;
611
+ const { countries = null, countryHint = null, context = null, chunkOffset = 0, referentialIntegrity = false, tokenize = false, detectDates = false, } = options;
517
612
  // A context makes the result depend on evidence from other chunks, so the
518
613
  // text no longer identifies the result. Caching is disabled rather than
519
614
  // keyed on the context, whose contents change as chunks arrive.
@@ -531,7 +626,7 @@ class EuRedact {
531
626
  // separator, and two different lists must never share a key.
532
627
  const allowKey = (allowed.size ? JSON.stringify([...allowed.keys()].sort()) : "")
533
628
  + (domains.size ? "|dom=" + JSON.stringify([...domains.keys()].sort()) : "");
534
- const cacheMode = `${mode}|dates=${detectDates}|hint=${hintKey}|ri=${referentialIntegrity}|tok=${tokenize}|allow=${allowKey}`;
629
+ const cacheMode = `rules|dates=${detectDates}|hint=${hintKey}|ri=${referentialIntegrity}|tok=${tokenize}|allow=${allowKey}`;
535
630
  let cacheKey;
536
631
  if (cache) {
537
632
  cacheKey = this.cache.key(normalizedText, countriesTuple, cacheMode);
@@ -1,11 +1,15 @@
1
1
  /**
2
2
  * [CLOUD EXTENSION] The cloud client.
3
3
  *
4
- * Speaks to the euRedact inference service: a deterministic rules engine
5
- * followed by a fine-tuned model asked only *what did the rules miss?* The
6
- * service returns both the redacted document and located spans, so this
4
+ * Speaks to the euRedact inference service: a fine-tuned model asked only
5
+ * *what did the rules miss?* The service returns located spans, so this
7
6
  * module's job is transport and translation, not detection.
8
7
  *
8
+ * It sends the text it is given, as given. `redactAsync(text, { mode: "cloud" })`
9
+ * is what runs the rules engine first and hands this client only the masked
10
+ * text; call the client directly and whatever you pass is what leaves the
11
+ * machine. The spans that come back index the text that was sent.
12
+ *
9
13
  * Three things it hides from the caller:
10
14
  *
11
15
  * - **Whether the document finished inside the service's sync window.** A short
@@ -26,7 +30,6 @@ export interface CloudRedactOptions {
26
30
  country: string;
27
31
  language?: string;
28
32
  priority?: "interactive" | "batch";
29
- rulesOnly?: boolean;
30
33
  idempotencyKey?: string;
31
34
  /** Injectable for tests. Defaults to the platform `fetch`. */
32
35
  fetchImpl?: typeof fetch;
@@ -1,11 +1,15 @@
1
1
  /**
2
2
  * [CLOUD EXTENSION] The cloud client.
3
3
  *
4
- * Speaks to the euRedact inference service: a deterministic rules engine
5
- * followed by a fine-tuned model asked only *what did the rules miss?* The
6
- * service returns both the redacted document and located spans, so this
4
+ * Speaks to the euRedact inference service: a fine-tuned model asked only
5
+ * *what did the rules miss?* The service returns located spans, so this
7
6
  * module's job is transport and translation, not detection.
8
7
  *
8
+ * It sends the text it is given, as given. `redactAsync(text, { mode: "cloud" })`
9
+ * is what runs the rules engine first and hands this client only the masked
10
+ * text; call the client directly and whatever you pass is what leaves the
11
+ * machine. The spans that come back index the text that was sent.
12
+ *
9
13
  * Three things it hides from the caller:
10
14
  *
11
15
  * - **Whether the document finished inside the service's sync window.** A short
@@ -153,7 +157,6 @@ export class CloudClient {
153
157
  country: options.country.toUpperCase(),
154
158
  language: options.language ?? "",
155
159
  priority: options.priority ?? "interactive",
156
- rules_only: options.rulesOnly ?? false,
157
160
  });
158
161
  let attempt = 0;
159
162
  for (;;) {
@@ -102,18 +102,50 @@ const SECRET_CONTEXT = [
102
102
  // ---------------------------------------------------------------------------
103
103
  // DOB context keywords (reused across SHARED patterns)
104
104
  // ---------------------------------------------------------------------------
105
+ // Every way the documents this engine sees introduce a date of birth.
106
+ //
107
+ // DOB recall was 62.8% -- 33,441 unmasked birth dates -- because this covered
108
+ // seven languages of thirty-one. Eleven countries scored exactly 100% (the
109
+ // listed languages) while twenty sat at 42-51%, which is what proved it was
110
+ // coverage rather than capability (rules-engine#38).
111
+ //
112
+ // Matching is substring, not word-boundary, so every entry was screened against
113
+ // the 29.2-million-character corpus for occurrences inside a longer word. Two
114
+ // were rejected: `fædd` lives inside `fæddur`, and `born` was found inside
115
+ // `gabornagy` and `gabornemeth` -- Hungarian names as e-mail local parts -- so
116
+ // `"born "` with the trailing space is listed instead.
117
+ //
118
+ // DOB_CONTEXT is gone: it was a second, shorter copy missing `nato il`,
119
+ // `nascido`, `geburtstag` and more, so ISO dates were gated on fewer keywords
120
+ // than DD/MM/YYYY ones for no stated reason.
105
121
  const DOB_CONTEXT = [
122
+ // Germanic / Romance -- the original set, kept verbatim.
106
123
  "geboren", "geboortedatum", "date de naissance", "né le", "née le",
107
124
  "né(e) le", "nee le", "nee(e) le", "date of birth", "DOB",
108
125
  "Geburtsdatum", "geboren am", "geboren op", "nascido", "nacido",
109
126
  "data di nascita", "nato il", "nata il", "geb.", "geb.datum", "geb ",
110
127
  "birth date", "birthday", "naissance", "geboorte", "geburtstag",
111
- ];
112
- const DOB_ISO_CONTEXT = [
113
- "geboren", "geboortedatum", "date de naissance", "né le", "née le",
114
- "né(e) le", "nee le", "date of birth", "DOB", "Geburtsdatum",
115
- "geboren am", "geboren op", "geb.", "birth date", "birthday",
116
- "naissance", "geboorte",
128
+ // English and Italian forms the list was missing.
129
+ "born ", "d.o.b", "nato/a il",
130
+ // Nordic.
131
+ "født", "fødselsdato", "född", "födelsedatum",
132
+ "syntynyt", "syntymäaika", "fæddur", "fæðingardagur",
133
+ // Greek -- Greece and Cyprus.
134
+ "γεννηθείς", "γεννήθηκε", "ημερομηνία γέννησης",
135
+ // West Slavic.
136
+ "urodzony", "urodzona", "data urodzenia",
137
+ "narozen", "datum narození", "narodený", "dátum narodenia",
138
+ // Hungarian, Romanian, Bulgarian.
139
+ "született", "születési", "născut", "născută", "data nașterii",
140
+ "роден", "родена", "дата на раждане",
141
+ // South Slavic.
142
+ "rođen", "rođena", "datum rođenja", "rojen", "rojena", "datum rojstva",
143
+ // Baltic.
144
+ "sündinud", "sünniaeg", "dzimis", "dzimusi", "dzimšanas datums",
145
+ "gimęs", "gimusi", "gimimo data",
146
+ // Maltese, Irish, Turkish (Cyprus).
147
+ "twieled", "twieldet", "data tat-twelid",
148
+ "rugadh", "dáta breithe", "doğum tarihi",
117
149
  ];
118
150
  const DATE_OF_DEATH_CONTEXT = [
119
151
  "overleden", "overlijdensdatum", "date de décès", "décédé le",
@@ -220,7 +252,12 @@ const SHARED = {
220
252
  // --- Secret / API Key (connection strings with embedded credentials) ---
221
253
  p(EntityType.SECRET, String.raw `(?:mongodb|mysql|postgres(?:ql)?|redis|amqp|rabbitmq):\/\/[^\s:]{1,256}:[^\s@]{1,256}@[^\s]+`, null, "Connection string with credentials"),
222
254
  // --- Secret / API Key (assignment-based: KEY=value or KEY: value) ---
223
- p(EntityType.SECRET, String.raw `(?<=[:=] )\S{8,}|(?<=[:=])\S{8,}`, "high_entropy", "Assigned secret value", SECRET_CONTEXT, true),
255
+ // The final character may not be sentence punctuation. `\S` does not stop
256
+ // at a full stop, so "Reisepass: CA1234567." was claimed as the
257
+ // ten-character span "CA1234567." -- a passport number plus the sentence's
258
+ // period, masked as a credential because a "credentials" word sat within
259
+ // the context window (rules-engine#35).
260
+ p(EntityType.SECRET, String.raw `(?<=[:=] )\S{7,}[^\s.,;:!?)'"]|(?<=[:=])\S{7,}[^\s.,;:!?)'"]`, "high_entropy", "Assigned secret value", SECRET_CONTEXT, true),
224
261
  // --- Secret / API Key (entropy-based fallback for longer tokens) ---
225
262
  // Only the *trailing* \b is replaced. It could not match when the token run
226
263
  // ended on "-", "+" or "/", so the engine backtracked across two
@@ -238,7 +275,7 @@ const SHARED = {
238
275
  p(EntityType.SECRET, String.raw `\b[A-Za-z0-9_\-+/]{24,}[A-Za-z0-9_\-+/=]*(?![A-Za-z0-9_\-+/=])`, "high_entropy", "High-entropy token", SECRET_CONTEXT, true),
239
276
  p(EntityType.DOB, String.raw `\b(?:0[1-9]|[12][0-9]|3[01])[/.\-](?:0[1-9]|1[0-2])[/.\-](?:19|20)\d{2}\b`, null, "DD/MM/YYYY", DOB_CONTEXT, true),
240
277
  p(EntityType.DATE_OF_DEATH, String.raw `\b(?:0[1-9]|[12][0-9]|3[01])[/.\-](?:0[1-9]|1[0-2])[/.\-](?:19|20)\d{2}\b`, null, "", DATE_OF_DEATH_CONTEXT, true),
241
- p(EntityType.DOB, String.raw `\b(?:19|20)\d{2}[/.\-](?:0[1-9]|1[0-2])[/.\-](?:0[1-9]|[12][0-9]|3[01])\b`, null, "ISO YYYY-MM-DD", DOB_ISO_CONTEXT, true),
278
+ p(EntityType.DOB, String.raw `\b(?:19|20)\d{2}[/.\-](?:0[1-9]|1[0-2])[/.\-](?:0[1-9]|[12][0-9]|3[01])\b`, null, "ISO YYYY-MM-DD", DOB_CONTEXT, true),
242
279
  ],
243
280
  };
244
281
  const NL = {
@@ -248,7 +285,7 @@ const NL = {
248
285
  p(EntityType.NATIONAL_ID, String.raw `\b[0-9]{9}\b`, "bsn"),
249
286
  p(EntityType.NATIONAL_ID, String.raw `\b[0-9]{2,4}\.[0-9]{2,4}\.[0-9]{2,4}\b`, "bsn"),
250
287
  p(EntityType.IBAN, String.raw `\bNL\d{2}\s?[A-Z]{4}\s?\d{4}\s?\d{4}\s?\d{2}\b`, "iban"),
251
- p(EntityType.VAT, String.raw `\bNL\d{9}B\d{2}\b`, "vat_nl"),
288
+ p(EntityType.VAT, String.raw `\bNL[\s.]?\d{9}B\d{2}\b`, "vat_nl"),
252
289
  p(EntityType.CHAMBER_OF_COMMERCE, String.raw `\b\d{8}\b`, "kvk", "", ["KvK", "Kamer van Koophandel", "kvk-nummer", "KVK-nummer", "handelsregister", "chamber of commerce"], true),
253
290
  p(EntityType.PHONE, String.raw `\b0[1-9]\d[\s\-]?\d{3}[\s\-]?\d{4}\b`),
254
291
  p(EntityType.PHONE, String.raw `\b0[1-9]\d{2}[\s\-]?\d{6,7}\b`),
@@ -275,7 +312,7 @@ const BE = {
275
312
  p(EntityType.PASSPORT, String.raw `\b[A-Z]{2}\d{6}\b`, null, "", PASSPORT_CONTEXT, true),
276
313
  p(EntityType.DRIVERS_LICENSE, String.raw `\b\d{10}\b`, null, "", ["rijbewijs", "permis de conduire", "driving licence", "driving license", "rijbewijsnummer"], true),
277
314
  p(EntityType.LICENSE_PLATE, String.raw `\b[12][\-\s]?[A-Z]{3}[\-\s]?\d{3}\b`),
278
- p(EntityType.POSTAL_CODE, String.raw `\b[1-9]\d{3}\b`, null, "", ["postcode", "code postal", "postnummer", "postal code", "zip", "B-", "adres", "adresse", "wonende", "woonplaats", "rue", "straat", "laan", "avenue", "boulevard", "plein", "steenweg", "chaussée", "domicilié", "gedomicilieerd", "Levering:", "siège"], true),
315
+ p(EntityType.POSTAL_CODE, String.raw `\b[1-9]\d{3}\b`, null, "", ["postcode", "code postal", "postnummer", "postal code", "zip", "B-", "adres", "adresse", "wonende", "woonplaats", "domicile", "rue", "straat", "laan", "avenue", "boulevard", "plein", "steenweg", "chaussée", "domicilié", "gedomicilieerd", "Levering:", "siège"], true),
279
316
  p(EntityType.POSTAL_CODE, String.raw `(?<=, )[1-9]\d{3}(?= [A-Z])`),
280
317
  p(EntityType.HEALTHCARE_PROVIDER, String.raw `\b\d{1}[\-.]?\d{5}[\-.]?\d{2}[\-.]?\d{3}\b`, null, "", ["RIZIV", "INAMI", "arts", "médecin", "zorgverlener", "prestataire", "dokter", "doctor"], true),
281
318
  ],
@@ -300,7 +337,7 @@ const DE = {
300
337
  // separates the city code: "B-AB 1234", "M-XY 99".
301
338
  p(EntityType.LICENSE_PLATE, String.raw `\b[A-ZÄÖÜ]{1,3}[\s\-][A-Z]{1,2}[\s\-]?\d{1,4}[EH]?\b`),
302
339
  p(EntityType.POSTAL_CODE, String.raw `\b\d{5}\b`, null, "", ["PLZ", "Postleitzahl", "postal code", "postcode", "Anschrift", "Adresse", "Wohnort", "Ort", "Stadt", "wohnt", "wohnhaft", "Straße", "Str.", "Weg", "Platz", "Allee", "Ring"], true),
303
- p(EntityType.CHAMBER_OF_COMMERCE, String.raw `\bHR[AB]\s?\d{4,6}\b`),
340
+ p(EntityType.CHAMBER_OF_COMMERCE, String.raw `\bHR[AB]\s?\d{4,6}(?:\s[A-Z]\b)?`),
304
341
  p(EntityType.SSN, String.raw `\b\d{2}[\s]?\d{6}[\s]?[A-Z][\s]?\d{3}\b`, null, "", ["Rentenversicherung", "RV-Nummer", "Sozialversicherung", "SV-Nummer", "Versicherungsnummer", "Rentenversicherungsnummer", "Sozialversicherungsnummer", "SVNR", "SV-Nr", "RVNR"], true),
305
342
  p(EntityType.TAX_ID, String.raw `\b\d{2,3}/\d{3,4}/\d{4,5}\b`, null, "", ["Steuernummer", "St.-Nr", "StNr", "Finanzamt", "Steuer"], true),
306
343
  p(EntityType.TAX_ID, String.raw `\b\d{13}\b`, null, "", ["Steuernummer", "St.-Nr", "StNr", "Finanzamt", "Steuer"], true),
@@ -313,7 +350,7 @@ const AT = {
313
350
  patterns: [
314
351
  p(EntityType.NATIONAL_ID, String.raw `\b\d{4}\s?\d{6}\b`, "austrian_svnr"),
315
352
  p(EntityType.IBAN, String.raw `\bAT\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\b`, "iban"),
316
- p(EntityType.VAT, String.raw `\bATU\d{8}\b`),
353
+ p(EntityType.VAT, String.raw `\bATU[\s.]?\d{8}\b`),
317
354
  p(EntityType.PHONE, String.raw `\b0\d{3,4}[\s\-]?\d{5,8}\b`),
318
355
  // Short area codes (Vienna is "01") split across three groups:
319
356
  // "01 53460 2215". The pattern above requires 3-4 digits after the trunk
@@ -354,7 +391,7 @@ const FR = {
354
391
  p(EntityType.PHONE, String.raw `\+33\s?[1-9][\s.\-]?\d{2}[\s.\-]?\d{2}[\s.\-]?\d{2}[\s.\-]?\d{2}`),
355
392
  p(EntityType.PASSPORT, String.raw `\b\d{2}[A-Z]{2}\d{5}\b`, null, "", PASSPORT_CONTEXT, true),
356
393
  p(EntityType.LICENSE_PLATE, String.raw `\b[A-Z]{2}[\-\s]?\d{3}[\-\s]?[A-Z]{2}\b`),
357
- p(EntityType.POSTAL_CODE, String.raw `\b(?:0[1-9]|[1-8]\d|9[0-5]|97[1-6])\d{3}\b`, null, "", ["code postal", "CP", "postal code", "postcode", "adresse", "domicilié", "résidant", "rue", "avenue", "boulevard", "place", "chemin", "allée", "impasse", "ville"], true),
394
+ p(EntityType.POSTAL_CODE, String.raw `\b(?:0[1-9]|[1-8]\d|9[0-5]|97[1-6])\d{3}\b`, null, "", ["code postal", "CP", "postal code", "postcode", "adresse", "domicile", "domicilié", "résidant", "rue", "avenue", "boulevard", "place", "chemin", "allée", "impasse", "ville"], true),
358
395
  p(EntityType.CHAMBER_OF_COMMERCE, String.raw `\b\d{3}\s?\d{3}\s?\d{3}\b`, null, "", ["SIREN", "siren", "RCS", "entreprise", "immatricul", "numéro d'entreprise", "registre du commerce"], true),
359
396
  p(EntityType.CHAMBER_OF_COMMERCE, String.raw `\b\d{3}\s?\d{3}\s?\d{3}\s?\d{5}\b`, null, "", ["SIRET", "siret", "établissement", "immatricul"], true),
360
397
  p(EntityType.TAX_ID, String.raw `\b\d{13}\b`, null, "", ["numéro fiscal", "SPI", "référence fiscale", "avis d'impôt", "impôt sur le revenu", "déclaration fiscale", "fiscal"], true),
@@ -387,9 +424,9 @@ const SE = {
387
424
  p(EntityType.PHONE, String.raw `\b0\d{1,3}[\-\s]?\d{2,3}[\s]?\d{2,3}[\s]?\d{2}\b`),
388
425
  p(EntityType.PHONE, String.raw `\+46\s?\d{1,3}[\s\-]?\d{2,3}[\s\-]?\d{2,3}[\s\-]?\d{2}`),
389
426
  p(EntityType.LICENSE_PLATE, String.raw `\b[A-Z]{3}\s?\d{2}[A-Z0-9]\b`),
390
- p(EntityType.POSTAL_CODE, String.raw `\b\d{3}\s?\d{2}\b`, null, "", ["postnummer", "postort", "postkod", "adress", "bostadsadress", "gatuadress", "boende", "gatan", "vägen", "gata", "väg", "allé", "plats", "torg", "stigen", "Bostadsadress"], true),
391
- p(EntityType.POSTAL_CODE, String.raw `\b\d{4}\s\d{2}\b`, null, "", ["postnummer", "postort", "postkod", "adress", "bostadsadress", "gatuadress", "boende", "gatan", "vägen", "gata", "väg", "allé"], true),
392
- p(EntityType.POSTAL_CODE, String.raw `\b\d{6}\b`, null, "", ["postnummer", "postort", "postkod", "adress", "bostadsadress", "gatuadress", "boende", "gatan", "vägen", "gata", "väg", "allé"], true),
427
+ p(EntityType.POSTAL_CODE, String.raw `\b\d{3}\s?\d{2}\b`, null, "", ["postnummer", "postort", "postkod", "adress", "Postal:", "Address:", "bostadsadress", "gatuadress", "boende", "gatan", "vägen", "gata", "väg", "allé", "plats", "torg", "stigen", "Bostadsadress"], true),
428
+ p(EntityType.POSTAL_CODE, String.raw `\b\d{4}\s\d{2}\b`, null, "", ["postnummer", "postort", "postkod", "adress", "Postal:", "Address:", "bostadsadress", "gatuadress", "boende", "gatan", "vägen", "gata", "väg", "allé"], true),
429
+ p(EntityType.POSTAL_CODE, String.raw `\b\d{6}\b`, null, "", ["postnummer", "postort", "postkod", "adress", "Postal:", "Address:", "bostadsadress", "gatuadress", "boende", "gatan", "vägen", "gata", "väg", "allé"], true),
393
430
  p(EntityType.CHAMBER_OF_COMMERCE, String.raw `\b\d{6}-?\d{4}\b`, null, "", ["organisationsnummer", "org.nr", "org nr", "Bolagsverket", "registreringsnummer"], true),
394
431
  ],
395
432
  };
@@ -405,7 +442,7 @@ const NO = {
405
442
  p(EntityType.PHONE, String.raw `\b[2-9]\d\s?\d{2}\s?\d{2}\s?\d{2}\b`),
406
443
  p(EntityType.PHONE, String.raw `\+47\s?\d{2}\s?\d{2}\s?\d{2}\s?\d{2}`),
407
444
  p(EntityType.LICENSE_PLATE, String.raw `\b[A-Z]{2}\s?\d{5}\b`),
408
- p(EntityType.POSTAL_CODE, String.raw `\b\d{4}\b`, null, "", ["postnummer", "poststed", "postnr", "adresse", "bostedsadresse", "gate", "vei", "veien", "gata", "plass", "stien", "allé"], true),
445
+ p(EntityType.POSTAL_CODE, String.raw `\b\d{4}\b`, null, "", ["postnummer", "poststed", "postnr", "adresse", "bostedsadresse", "gate", "vei", "veien", "gata", "plass", "stien", "allé", "Postal:", "Address:"], true),
409
446
  ],
410
447
  };
411
448
  const FI = {
@@ -414,7 +451,7 @@ const FI = {
414
451
  patterns: [
415
452
  p(EntityType.NATIONAL_ID, String.raw `\b\d{6}[-+ABCDEFYXWVU]\d{3}[0-9A-FHJK-NPR-Y]\b`, "finnish_hetu"),
416
453
  p(EntityType.IBAN, String.raw `\bFI\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{2}\b`, "iban"),
417
- p(EntityType.VAT, String.raw `\bFI\d{8}\b`),
454
+ p(EntityType.VAT, String.raw `\bFI[\s.]?\d{8}\b`),
418
455
  p(EntityType.CHAMBER_OF_COMMERCE, String.raw `\b\d{7}-\d\b`, "finnish_business_id"),
419
456
  p(EntityType.PHONE, String.raw `\b0\d{1,2}\s?\d{3,4}\s?\d{3,4}\b`),
420
457
  p(EntityType.PHONE, String.raw `\+358\s?\d{1,3}[\s\-]?\d{3,4}[\s\-]?\d{3,4}`),
@@ -432,7 +469,7 @@ const IS = {
432
469
  p(EntityType.PHONE, String.raw `\b[3-9]\d{6}\b`, null, "", ["sími", "símanúmer", "farsími", "gsm", "phone", "tel", "hringja", "hringdu", "ná í", "nás á", "nás", "SMS", "sent to"], true),
433
470
  p(EntityType.PHONE, String.raw `\+354\s?\d{3}\s?\d{4}`),
434
471
  p(EntityType.LICENSE_PLATE, String.raw `\b[A-Z]{2}\s?\d{3}\b`, null, "", ["skráningarnúmer", "bílnúmer", "ökutæki", "bifreið", "plata"], true),
435
- p(EntityType.POSTAL_CODE, String.raw `\b[1-9]\d{2}\b`, null, "", ["póstnúmer", "póstfang", "staður", "heimilisfang"], true),
472
+ p(EntityType.POSTAL_CODE, String.raw `\b[1-9]\d{2}\b`, null, "", ["póstnúmer", "póstfang", "staður", "heimilisfang", "Postal:", "Address:"], true),
436
473
  ],
437
474
  };
438
475
  const IT = {
@@ -442,7 +479,7 @@ const IT = {
442
479
  p(EntityType.NATIONAL_ID, String.raw `\b[A-Z]{6}\d{2}[ABCDEHLMPRST]\d{2}[A-Z]\d{3}[A-Z]\b`, "italian_cf"),
443
480
  p(EntityType.IBAN, String.raw `\bIT\d{2}\s?[A-Z]\d{3}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{3}\b`, "iban"),
444
481
  p(EntityType.IBAN, String.raw `\bIT\d{2}[A-Z]\d{22}\b`, "iban"),
445
- p(EntityType.VAT, String.raw `\bIT\d{11}\b`),
482
+ p(EntityType.VAT, String.raw `\bIT[\s.]?\d{11}\b`),
446
483
  p(EntityType.PHONE, String.raw `\b3\d{2}[\s\-]?\d{3}[\s\-]?\d{4}\b`),
447
484
  p(EntityType.PHONE, String.raw `\b0\d{1,3}[\s\-]?\d{6,8}\b`),
448
485
  p(EntityType.PHONE, String.raw `\b0\d{1,2}[\s\-]\d{3}[\s\-]\d{4}\b`),
@@ -458,7 +495,7 @@ const ES = {
458
495
  p(EntityType.NATIONAL_ID, String.raw `\b\d{8}[A-Z]\b`, "spanish_dni"),
459
496
  p(EntityType.NATIONAL_ID, String.raw `\b[XYZ]\d{7}[A-Z]\b`, "spanish_nie"),
460
497
  p(EntityType.IBAN, String.raw `\bES\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\b`, "iban"),
461
- p(EntityType.VAT, String.raw `\bES[A-Z0-9]\d{7}[A-Z0-9]\b`),
498
+ p(EntityType.VAT, String.raw `\bES[\s.]?[A-Z0-9]\d{7}[A-Z0-9]\b`),
462
499
  p(EntityType.PHONE, String.raw `\b[679]\d{2}[\s\-]?\d{3}[\s\-]?\d{3}\b`),
463
500
  p(EntityType.PHONE, String.raw `\b[679]\d{8}\b`),
464
501
  p(EntityType.PHONE, String.raw `\b[679]\d[\s\-]?\d{3}[\s\-]?\d{2}[\s\-]?\d{2}\b`),
@@ -481,7 +518,7 @@ const PT = {
481
518
  p(EntityType.NATIONAL_ID, String.raw `\b[1-35-9]\d{8}\b`, "portuguese_nif"),
482
519
  p(EntityType.NATIONAL_ID, String.raw `\b[1-35-9]\d{2}[\s.]\d{3}[\s.]\d{3}\b`, "portuguese_nif"),
483
520
  p(EntityType.IBAN, String.raw `\bPT\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{5}\b`, "iban"),
484
- p(EntityType.VAT, String.raw `\bPT\d{9}\b`),
521
+ p(EntityType.VAT, String.raw `\bPT[\s.]?\d{9}\b`),
485
522
  p(EntityType.PHONE, String.raw `\b[29]\d{2}[\s\-]?\d{3}[\s\-]?\d{3}\b`),
486
523
  p(EntityType.PHONE, String.raw `\b[29]\d{8}\b`),
487
524
  p(EntityType.PHONE, String.raw `\+351\s?[29]\d{2}[\s\-]?\d{3}[\s\-]?\d{3}`),
@@ -514,7 +551,7 @@ const PL = {
514
551
  p(EntityType.TAX_ID, String.raw `\b\d{3}-?\d{3}-?\d{2}-?\d{2}\b`, "polish_nip"),
515
552
  p(EntityType.IBAN, String.raw `\bPL\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\b`, "iban"),
516
553
  p(EntityType.IBAN, String.raw `\bPL\d{26}\b`, "iban"),
517
- p(EntityType.VAT, String.raw `\bPL\d{10}\b`),
554
+ p(EntityType.VAT, String.raw `\bPL[\s.]?\d{10}\b`),
518
555
  p(EntityType.PHONE, String.raw `\b[5-8]\d{2}[\s\-]?\d{3}[\s\-]?\d{3}\b`),
519
556
  p(EntityType.PHONE, String.raw `\b[5-8]\d{8}\b`),
520
557
  p(EntityType.PHONE, String.raw `\+48\s?[5-8]\d{2}[\s\-]?\d{3}[\s\-]?\d{3}`),
@@ -528,7 +565,7 @@ const IE = {
528
565
  p(EntityType.NATIONAL_ID, String.raw `\b\d{7}[A-W][ABWTXZ]?\b`, "irish_pps"),
529
566
  p(EntityType.IBAN, String.raw `\bIE\d{2}\s?[A-Z]{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{2}\b`, "iban"),
530
567
  p(EntityType.IBAN, String.raw `\bIE\d{2}[A-Z]{4}\d{14}\b`, "iban"),
531
- p(EntityType.VAT, String.raw `\bIE\d{7}[A-Z]\b`),
568
+ p(EntityType.VAT, String.raw `\bIE[\s.]?\d{7}[A-Z]\b`),
532
569
  p(EntityType.PHONE, String.raw `\b0[89]\d[\s\-]?\d{3}[\s\-]?\d{4}\b`),
533
570
  p(EntityType.PHONE, String.raw `\b0[89]\d{8}\b`),
534
571
  p(EntityType.PHONE, String.raw `\b0[1-9]\d{0,2}[\s\-]?\d{5,8}\b`),
@@ -544,7 +581,7 @@ const UK = {
544
581
  p(EntityType.HEALTH_INSURANCE, String.raw `\b\d{3}\s?\d{3}\s?\d{4}\b`, "uk_nhs", "", ["NHS", "NHS number", "health number", "NHS no"], true),
545
582
  p(EntityType.IBAN, String.raw `\bGB\d{2}\s?[A-Z]{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{2}\b`, "iban"),
546
583
  p(EntityType.IBAN, String.raw `\bGB\d{2}[A-Z]{4}\d{14}\b`, "iban"),
547
- p(EntityType.VAT, String.raw `\bGB\d{9}\b`),
584
+ p(EntityType.VAT, String.raw `\bGB[\s.]?\d{3}[\s.]?\d{4}[\s.]?\d{2}\b`),
548
585
  p(EntityType.PHONE, String.raw `\b07[4-9]\d{2}[\s\-]?\d{3}[\s\-]?\d{3}\b`),
549
586
  p(EntityType.PHONE, String.raw `\b07[4-9]\d{8}\b`),
550
587
  p(EntityType.PHONE, String.raw `\b0[12]\d{2}[\s\-]?\d{3}[\s\-]?\d{4}\b`),
@@ -560,7 +597,7 @@ const BG = {
560
597
  patterns: [
561
598
  p(EntityType.NATIONAL_ID, String.raw `\b\d{10}\b`, "bulgarian_egn"),
562
599
  p(EntityType.IBAN, String.raw `\bBG\d{2}\s?[A-Z]{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{2}\b`, "iban"),
563
- p(EntityType.VAT, String.raw `\bBG\d{9,10}\b`),
600
+ p(EntityType.VAT, String.raw `\bBG[\s.]?\d{9,10}\b`),
564
601
  p(EntityType.PHONE, String.raw `\b0?[89][789][\s\-]?\d{3}[\s\-]?\d{3,4}\b`),
565
602
  p(EntityType.PHONE, String.raw `\b0?[89][789]\d{7,8}\b`),
566
603
  p(EntityType.PHONE, String.raw `\+359\s?[89][789][\s\-]?\d{3}[\s\-]?\d{3,4}`),
@@ -575,7 +612,7 @@ const CY = {
575
612
  p(EntityType.NATIONAL_ID, String.raw `\b\d{8}[A-Z]\b`),
576
613
  p(EntityType.IBAN, String.raw `\bCY\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\b`, "iban"),
577
614
  p(EntityType.IBAN, String.raw `\bCY\d{26}\b`, "iban"),
578
- p(EntityType.VAT, String.raw `\bCY\d{8}[A-Z]\b`),
615
+ p(EntityType.VAT, String.raw `\bCY[\s.]?\d{8}[A-Z]\b`),
579
616
  p(EntityType.PHONE, String.raw `\b[29]\d{3}[\s\-]?\d{4}\b`),
580
617
  p(EntityType.PHONE, String.raw `\b[29]\d[\s\-]?\d{3}[\s\-]?\d{3}\b`),
581
618
  p(EntityType.PHONE, String.raw `\b[29]\d{7}\b`),
@@ -591,7 +628,7 @@ const CZ = {
591
628
  p(EntityType.NATIONAL_ID, String.raw `\b\d{6}/?\d{3,4}\b`, "czech_birth_number"),
592
629
  p(EntityType.IBAN, String.raw `\bCZ\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\b`, "iban"),
593
630
  p(EntityType.IBAN, String.raw `\bCZ\d{22}\b`, "iban"),
594
- p(EntityType.VAT, String.raw `\bCZ\d{8,10}\b`),
631
+ p(EntityType.VAT, String.raw `\bCZ[\s.]?\d{8,10}\b`),
595
632
  p(EntityType.PHONE, String.raw `\b[67]\d{2}[\s\-]?\d{3}[\s\-]?\d{3}\b`),
596
633
  p(EntityType.PHONE, String.raw `\b[67]\d{8}\b`),
597
634
  p(EntityType.PHONE, String.raw `\+420\s?[67]\d{2}[\s\-]?\d{3}[\s\-]?\d{3}`),
@@ -605,7 +642,7 @@ const SK = {
605
642
  p(EntityType.NATIONAL_ID, String.raw `\b\d{6}/?\d{3,4}\b`, "czech_birth_number"),
606
643
  p(EntityType.IBAN, String.raw `\bSK\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\b`, "iban"),
607
644
  p(EntityType.IBAN, String.raw `\bSK\d{22}\b`, "iban"),
608
- p(EntityType.VAT, String.raw `\bSK\d{10}\b`),
645
+ p(EntityType.VAT, String.raw `\bSK[\s.]?\d{10}\b`),
609
646
  p(EntityType.PHONE, String.raw `\b0?9\d{2}[\s\-]?\d{3}[\s\-]?\d{3}\b`),
610
647
  p(EntityType.PHONE, String.raw `\+421\s?9\d{2}[\s\-]?\d{3}[\s\-]?\d{3}`),
611
648
  p(EntityType.POSTAL_CODE, String.raw `\b\d{3}\s?\d{2}\b`, null, "", ["PSČ", "poštové", "adresa", "ulica", "Postal:", "Address:"], true),
@@ -618,7 +655,7 @@ const EE = {
618
655
  p(EntityType.NATIONAL_ID, String.raw `\b[1-6]\d{10}\b`, "estonian_id"),
619
656
  p(EntityType.IBAN, String.raw `\bEE\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\b`, "iban"),
620
657
  p(EntityType.IBAN, String.raw `\bEE\d{18}\b`, "iban"),
621
- p(EntityType.VAT, String.raw `\bEE\d{9}\b`),
658
+ p(EntityType.VAT, String.raw `\bEE[\s.]?\d{9}\b`),
622
659
  p(EntityType.PHONE, String.raw `\b5\d{3}[\s\-]?\d{4}\b`),
623
660
  p(EntityType.PHONE, String.raw `\b5\d{7}\b`),
624
661
  p(EntityType.PHONE, String.raw `\+372\s?5\d{3}[\s\-]?\d{3,4}`),
@@ -632,7 +669,7 @@ const LT = {
632
669
  p(EntityType.NATIONAL_ID, String.raw `\b[1-6]\d{10}\b`, "estonian_id"),
633
670
  p(EntityType.IBAN, String.raw `\bLT\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\b`, "iban"),
634
671
  p(EntityType.IBAN, String.raw `\bLT\d{18}\b`, "iban"),
635
- p(EntityType.VAT, String.raw `\bLT\d{9,12}\b`),
672
+ p(EntityType.VAT, String.raw `\bLT[\s.]?\d{9,12}\b`),
636
673
  p(EntityType.PHONE, String.raw `\b6\d{2}[\s\-]?\d{2}[\s\-]?\d{3}\b`),
637
674
  p(EntityType.PHONE, String.raw `\b6\d{7}\b`),
638
675
  p(EntityType.PHONE, String.raw `\b8[\s\-]?6\d{2}[\s\-]?\d{2}[\s\-]?\d{3}\b`),
@@ -648,7 +685,7 @@ const LV = {
648
685
  p(EntityType.NATIONAL_ID, String.raw `\b(?:0[1-9]|[12]\d|3[01])(?:0[1-9]|1[0-2])\d{7}\b`, null, "", ["personas kods", "PK", "identifikācijas"], true),
649
686
  p(EntityType.IBAN, String.raw `\bLV\d{2}\s?[A-Z]{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d\b`, "iban"),
650
687
  p(EntityType.IBAN, String.raw `\bLV\d{2}[A-Z]{4}\d{13}\b`, "iban"),
651
- p(EntityType.VAT, String.raw `\bLV\d{11}\b`),
688
+ p(EntityType.VAT, String.raw `\bLV[\s.]?\d{11}\b`),
652
689
  p(EntityType.PHONE, String.raw `\b[2]\d{3}[\s\-]?\d{4}\b`),
653
690
  p(EntityType.PHONE, String.raw `\b[2]\d{7}\b`),
654
691
  p(EntityType.PHONE, String.raw `\+371\s?[2]\d{3}[\s\-]?\d{3,4}`),
@@ -663,7 +700,7 @@ const HR = {
663
700
  p(EntityType.IBAN, String.raw `\bHR\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{3}\b`, "iban"),
664
701
  p(EntityType.IBAN, String.raw `\bHR\d{2}\s\d{4}\s\d{4}\s\d{4}\s\d{5}\b`, "iban"),
665
702
  p(EntityType.IBAN, String.raw `\bHR\d{19}\b`, "iban"),
666
- p(EntityType.VAT, String.raw `\bHR\d{11}\b`),
703
+ p(EntityType.VAT, String.raw `\bHR[\s.]?\d{11}\b`),
667
704
  p(EntityType.PHONE, String.raw `\b0?9[12579][\s\-]?\d{3}[\s\-]?\d{3,4}\b`),
668
705
  p(EntityType.PHONE, String.raw `\b0?9[12579]\d{7,8}\b`),
669
706
  p(EntityType.PHONE, String.raw `\+385\s?9[12579][\s\-]?\d{3}[\s\-]?\d{3,4}`),
@@ -677,7 +714,7 @@ const HU = {
677
714
  p(EntityType.NATIONAL_ID, String.raw `\b\d{9}\b`, "hungarian_taj"),
678
715
  p(EntityType.NATIONAL_ID, String.raw `\b\d{3}\s\d{3}\s\d{3}\b`, "hungarian_taj"),
679
716
  p(EntityType.TAX_ID, String.raw `\b8\d{9}\b`, null, "", ["adóazonosító", "adószám", "adóazonosító jel"], true),
680
- p(EntityType.VAT, String.raw `\bHU\d{8,11}\b`, null, "", ["adószám", "ÁFA"]),
717
+ p(EntityType.VAT, String.raw `\bHU[\s.]?\d{8,11}\b`, null, "", ["adószám", "ÁFA"]),
681
718
  p(EntityType.IBAN, String.raw `\bHU\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\b`, "iban"),
682
719
  p(EntityType.IBAN, String.raw `\bHU\d{26}\b`, "iban"),
683
720
  p(EntityType.PHONE, String.raw `\b06\s?\d{1,2}[\s\-]?\d{3}[\s\-]?\d{3,4}\b`),
@@ -692,7 +729,7 @@ const RO = {
692
729
  patterns: [
693
730
  p(EntityType.NATIONAL_ID, String.raw `\b[1-8]\d{12}\b`, "romanian_cnp"),
694
731
  p(EntityType.IBAN, String.raw `\bRO\d{2}[A-Z]{4}\d{16}\b`, "iban"),
695
- p(EntityType.VAT, String.raw `\bRO\d{2,10}\b`),
732
+ p(EntityType.VAT, String.raw `\bRO[\s.]?\d{2,10}\b`),
696
733
  p(EntityType.PHONE, String.raw `\b0?7[2-9]\d{1}[\s\-]?\d{3}[\s\-]?\d{3,4}\b`),
697
734
  p(EntityType.PHONE, String.raw `\b0?7[2-9]\d{7,8}\b`),
698
735
  p(EntityType.PHONE, String.raw `\+40\s?7[2-9]\d{1}[\s\-]?\d{3}[\s\-]?\d{3,4}`),
@@ -707,7 +744,7 @@ const EL = {
707
744
  p(EntityType.NATIONAL_ID, String.raw `\b\d{11}\b`, "greek_amka"),
708
745
  p(EntityType.IBAN, String.raw `\bGR\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{3}\b`, "iban"),
709
746
  p(EntityType.IBAN, String.raw `\bGR\d{25}\b`, "iban"),
710
- p(EntityType.VAT, String.raw `\bEL\d{9}\b`),
747
+ p(EntityType.VAT, String.raw `\bEL[\s.]?\d{9}\b`),
711
748
  p(EntityType.PHONE, String.raw `\b69\d[\s\-]?\d{3,4}[\s\-]?\d{4}\b`),
712
749
  p(EntityType.PHONE, String.raw `\b69\d{8}\b`),
713
750
  p(EntityType.PHONE, String.raw `\b2\d{1,3}[\s\-]?\d{3,4}[\s\-]?\d{3,4}\b`),
@@ -722,7 +759,7 @@ const SI = {
722
759
  p(EntityType.NATIONAL_ID, String.raw `\b\d{13}\b`, "slovenian_emso"),
723
760
  p(EntityType.IBAN, String.raw `\bSI\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{3}\b`, "iban"),
724
761
  p(EntityType.IBAN, String.raw `\bSI\d{17}\b`, "iban"),
725
- p(EntityType.VAT, String.raw `\bSI\d{8}\b`),
762
+ p(EntityType.VAT, String.raw `\bSI[\s.]?\d{8}\b`),
726
763
  p(EntityType.PHONE, String.raw `\b0?[34567]\d{1}[\s\-]?\d{3}[\s\-]?\d{3}\b`),
727
764
  p(EntityType.PHONE, String.raw `\b0?[34567]\d{7}\b`),
728
765
  p(EntityType.PHONE, String.raw `\+386\s?[34567]\d{1}[\s\-]?\d{3}[\s\-]?\d{3}`),
@@ -737,7 +774,7 @@ const MT = {
737
774
  p(EntityType.NATIONAL_ID, String.raw `\b\d{5,7}[MGAPLHBZ]\b`),
738
775
  p(EntityType.IBAN, String.raw `\bMT\d{2}\s?[A-Z]{4}\s?\d{4}\s?\d{4}\s?[A-Z0-9]{4}\s?[A-Z0-9]{4}\s?[A-Z0-9]{4}\s?[A-Z0-9]{3}\b`, "iban"),
739
776
  p(EntityType.IBAN, String.raw `\bMT\d{2}[A-Z]{4}\d{5}[A-Z0-9]{18}\b`, "iban"),
740
- p(EntityType.VAT, String.raw `\bMT\d{8}\b`),
777
+ p(EntityType.VAT, String.raw `\bMT[\s.]?\d{8}\b`),
741
778
  p(EntityType.PHONE, String.raw `\b[79]\d{3}[\s\-]?\d{4}\b`),
742
779
  p(EntityType.PHONE, String.raw `\b[79]\d{7}\b`),
743
780
  p(EntityType.PHONE, String.raw `\+356\s?[79]\d{3}[\s\-]?\d{3,4}`),