]> git.99rst.org Git - sgasser-llm-shield.git/commitdiff
Default phone detection to international formats (#111)
authorStefan Gasser <redacted>
Tue, 23 Jun 2026 10:07:38 +0000 (12:07 +0200)
committerGitHub <redacted>
Tue, 23 Jun 2026 10:07:38 +0000 (12:07 +0200)
* Default phone detection to international formats

* Update mask API phone example

12 files changed:
README.md
benchmarks/pii-accuracy/test-data/hard.yaml
config.example.yaml
detector/detector/deterministic.py
detector/tests/test_deterministic.py
docs/api-reference/mask.mdx
docs/api-reference/status.mdx
docs/concepts/pii-detection.mdx
docs/configuration/pii-detection.mdx
src/config.test.ts
src/config.ts
src/index.ts

index 0b432cfedfd28f7fbe18da34479cc10e60f88400..147804822a8aea7e757d6eb5391da2e850809589 100644 (file)
--- a/README.md
+++ b/README.md
@@ -127,7 +127,7 @@ Both detected and masked in real time, including streaming responses.
 
 ## How detection works
 
-Detection runs as a separate service that PasteGuard calls over HTTP, so you can run it wherever you like. It mixes two things: exact checks with checksums (IBANs, credit cards, emails, phones, IPs) and a small AI model ([GLiNER](https://github.com/urchade/GLiNER)) for names and places. It works the same in any language. National-format phone numbers are validated against configured phone regions; international `+` numbers work globally.
+Detection runs as a separate service that PasteGuard calls over HTTP, so you can run it wherever you like. It mixes exact checks with checksums (IBANs, credit cards, emails, phones, IPs) and a small AI model ([GLiNER](https://github.com/urchade/GLiNER)) for names and places. It works the same in any language. Phone numbers are international-only by default; add `phone_regions` if you need local formats.
 
 Code, Docker image, and tests are in [`detector/`](detector/).
 
index 0545ce30b1112b48296013b624b14c5b940db51d..02dc95d64bea5eeeb07428b10a4631546f3d4e3b 100644 (file)
@@ -65,15 +65,15 @@ cases:
         text: Anne-Marie Johnson
         match: contains
 
-  - id: hard_phone_national_uk
+  - id: hard_phone_international_uk
     suite: hard
     category: hard
     split: test
     language: en
-    text: "The London callback number is 020 7946 0958."
+    text: "The London callback number is +44 20 7946 0958."
     expected:
       - entity: PHONE_NUMBER
-        text: 020 7946 0958
+        text: +44 20 7946 0958
         match: contains
 
   - id: hard_location_ambiguous_city
index accc236658dc16a23b88d56e3ac1873c0ac20c2f..cd1698ffde1f60f3be8e44a1c85d1de2bc11ca15 100644 (file)
@@ -71,25 +71,9 @@ pii_detection:
   # for local dev (bun run dev against the docker-compose detector service).
   detector_url: ${DETECTOR_URL:-http://localhost:5002}
 
-  # Regions for national-format phone numbers; +international numbers work globally.
-  phone_regions:
-    - US
-    - GB
-    - DE
-    - AT
-    - CH
-    - IT
-    - FR
-    - BE
-    - LU
-    - ES
-    - NL
-    - PT
-    - BR
-    - PL
-    - RO
-    - MD
-    - IN
+  # Add regions only if you need national-format numbers; + numbers work globally.
+  phone_regions: []
+  # phone_regions: [US, GB, DE, IT, IN]
 
   score_threshold: 0.7  # Minimum confidence score (0.0 - 1.0)
 
index dbc1a49c2f8eb761d09e8f42694da4aae648a7ea..052a63273a22d046b97155fe376d686a6716abad 100644 (file)
@@ -27,26 +27,6 @@ from .entities import (
     overlaps,
 )
 
-DEFAULT_PHONE_REGIONS = [
-    "US",
-    "GB",
-    "DE",
-    "AT",
-    "CH",
-    "IT",
-    "FR",
-    "BE",
-    "LU",
-    "ES",
-    "NL",
-    "PT",
-    "BR",
-    "PL",
-    "RO",
-    "MD",
-    "IN",
-]
-
 # `\w` is Unicode-aware so accented names (müller@, andré.) match in full;
 # structure rejects leading/trailing/consecutive dots.
 _EMAIL_RE = re.compile(
@@ -156,7 +136,7 @@ def _phone_regions(phone_regions: list[str] | None) -> list[str]:
             seen.add(region)
         return normalized
 
-    return DEFAULT_PHONE_REGIONS
+    return []
 
 
 def _phone(text: str, phone_regions: list[str] | None = None) -> list[Span]:
index 9ce051c76e0ff4bd79669cc57f4661afb41783f0..d2194a7ba368db03f694d729ea31d40cd2a95170 100644 (file)
@@ -205,8 +205,8 @@ def test_phone_regions_control_national_formats():
     )
 
 
-def test_phone_default_regions_keep_longest_overlap():
-    types = types_texts("Please call the customer on 98765 43210.")
+def test_phone_configured_regions_keep_longest_overlap():
+    types = types_texts("Please call the customer on 98765 43210.", ["IN"])
     assert (PHONE_NUMBER, "98765 43210") in types
     assert (PHONE_NUMBER, "43210") not in types
 
@@ -215,6 +215,12 @@ def test_phone_no_false_positive_on_invoice_number():
     assert all(t != PHONE_NUMBER for t, _ in types_texts("Rechnung 2893081508152 vom"))
 
 
+def test_default_phone_detection_is_international_only():
+    assert (PHONE_NUMBER, "+49 171 1234567") in types_texts("Tel: +49 171 1234567")
+    assert all(t != PHONE_NUMBER for t, _ in types_texts("Telefon 0171-1234567"))
+    assert all(t != PHONE_NUMBER for t, _ in types_texts("Please call 98765 43210."))
+
+
 def test_phone_english_uk_national():
     assert (PHONE_NUMBER, "0121 234 5678") in types_texts(
         "The Birmingham callback number is 0121 234 5678.", ["GB"]
index 3d4ebd21f9a41f34125e5984cc5eb250bf01da07..6bb71f6430b93fe7bd9ddfebb3563978b2172910 100644 (file)
@@ -22,7 +22,7 @@ POST /api/mask
 curl -X POST http://localhost:3000/api/mask \
   -H "Content-Type: application/json" \
   -d '{
-    "text": "Contact john@example.com or call 555-1234"
+    "text": "Contact john@example.com or call +1 415-555-1234"
   }'
 ```
 
@@ -47,7 +47,7 @@ X-PasteGuard-Source: browser-extension
   "masked": "Contact [[EMAIL_ADDRESS_1]] or call [[PHONE_NUMBER_1]]",
   "context": {
     "[[EMAIL_ADDRESS_1]]": "john@example.com",
-    "[[PHONE_NUMBER_1]]": "555-1234"
+    "[[PHONE_NUMBER_1]]": "+1 415-555-1234"
   },
   "counters": {
     "EMAIL_ADDRESS": 1,
index 0a5d4e85f0f135da64665af981918b4abf78b0ae..72e898e2f746a8923fea9e676229431195342bf7 100644 (file)
@@ -81,7 +81,7 @@ curl http://localhost:3000/info
     }
   },
   "pii_detection": {
-    "phone_regions": ["US", "GB", "DE", "IT", "IN"],
+    "phone_regions": [],
     "score_threshold": 0.7,
     "entities": ["PERSON", "EMAIL_ADDRESS", "PHONE_NUMBER"]
   },
index acabefb7cb8b33c6c35b1b7bc423facc17c7e77d..e821c3341449528cb18ed875959cae67ad4c4511 100644 (file)
@@ -25,13 +25,13 @@ The detector speaks the `/analyze` HTTP contract, is configured through the `det
 | `IP_ADDRESS` | 192.168.1.1 |
 | `VAT_CODE` | EU VAT number, e.g. `DE136695976`, `IT00743110157`, `FR40303265045` |
 
-Names and locations are detected by the multilingual model (verified across EN/DE/IT/FR/ES/NL/PT and more). IBAN, credit card, email, and IP work internationally. Phone numbers with a `+` country prefix are global; national-format phone numbers are validated against configured `phone_regions`. `VAT_CODE` covers any EU member state, validated per-country via the VAT checksum (`python-stdnum`) and requiring the country prefix. The structured identifiers (IBAN, credit card, IP, VAT) are checksum- or format-validated, so they are caught with high precision and without false positives on lookalike numbers.
+Names and locations use the multilingual model. IBAN, credit card, email, and IP work internationally. Phone numbers are `+` international-only by default; set `phone_regions` for local formats. `VAT_CODE` requires a country prefix and is validated with `python-stdnum`.
 
 ## Languages and phone regions
 
-Detection is multilingual and **language-agnostic** — one pass over the text finds entities regardless of the language it is written in, so mixed-language documents (for example an Italian name and city in a German letter) are handled correctly. PasteGuard does not auto-detect language.
+Detection is multilingual and **language-agnostic**. PasteGuard does not auto-detect language.
 
-National-format phone numbers are the exception because the same digits can be valid or invalid depending on the country numbering plan. Configure `phone_regions` for the regions your traffic commonly contains.
+National-format phone numbers need country rules. Configure `phone_regions` only for regions your traffic uses.
 
 ## Confidence Scoring
 
index b53ded681db024bf00ea642c733ca487ef809afa..b3ee39ae75e806edcaaba6095a824447c3eb90e6 100644 (file)
@@ -6,12 +6,7 @@ description: Configure PII detection settings
 ```yaml
 pii_detection:
   detector_url: http://localhost:5002
-  phone_regions:
-    - US
-    - GB
-    - DE
-    - IT
-    - IN
+  phone_regions: []
   score_threshold: 0.7
   entities:
     - PERSON
@@ -29,15 +24,15 @@ pii_detection:
 | Option | Default | Description |
 |--------|---------|-------------|
 | `detector_url` | `http://localhost:5002` | Detector `/analyze` URL |
-| `phone_regions` | US, GB, DE, AT, CH, IT, FR, BE, LU, ES, NL, PT, BR, PL, RO, MD, IN | Regions used to validate national-format phone numbers without a country prefix |
+| `phone_regions` | `[]` | Optional regions for national-format phone numbers |
 | `score_threshold` | `0.7` | Minimum confidence floor for the neural labels PERSON and LOCATION (0.0-1.0). Checksum-validated identifiers always score `1.0` and are unaffected |
 | `entities` | See below | Entity types to return |
 
 ## Phone Regions
 
-Detection is **multilingual and language-agnostic** — the model finds names and places regardless of the language the text is written in (an Italian name and city in a German letter are caught), and structured identifiers are matched by checksum independent of language. There are no per-language images and no spaCy models to load.
+Detection is **multilingual and language-agnostic**. Names and places use one model. Structured identifiers use format or checksum checks.
 
-Phone numbers are different: international numbers with a `+` country prefix are detected globally, but national-format numbers need one or more country numbering plans. Configure those plans with `phone_regions` instead of relying on text language.
+Phone numbers are `+` international-only by default. Add `phone_regions` only when you need local formats.
 
 ```yaml
 pii_detection:
@@ -49,7 +44,7 @@ pii_detection:
     - IN
 ```
 
-Use a focused list for the traffic you expect. Adding many regions improves recall for mixed-language and international text, but can increase false positives on IDs, ticket numbers, and other digit sequences.
+Keep the list focused. More regions can mean more false positives on IDs and ticket numbers.
 
 ## Entities
 
@@ -64,7 +59,7 @@ Use a focused list for the traffic you expect. Adding many regions improves reca
 | `IP_ADDRESS` | 192.168.1.1 |
 | `VAT_CODE` | EU VAT number, e.g. `DE136695976`, `IT00743110157`, `FR40303265045` |
 
-Names and locations are multilingual. IBAN, credit card, phone, email, and IP are international. `VAT_CODE` covers any EU member state — validated per-country via the VAT checksum (`python-stdnum`) and requiring the country prefix; a bare domestic number without it is not flagged. The structured identifiers (IBAN, credit card, IP, VAT) are checksum- or format-validated, so they are caught with high precision and without false positives on lookalike numbers.
+Names and locations are multilingual. IBAN, credit card, email, IP, and `+` phone numbers work internationally. `VAT_CODE` requires a country prefix and is validated with `python-stdnum`.
 
 ## Score Threshold
 
index 315ef1aba5ba5eec18c96c8e44b9184fcd87d610..b55d857a1edc2c4bad8218804b8dccaddaca7d5a 100644 (file)
@@ -144,6 +144,25 @@ pii_detection:
     }
   });
 
+  test("defaults to international-only phone detection", () => {
+    const path = writeConfig(`
+mode: mask
+providers:
+  openai: {}
+  anthropic: {}
+pii_detection:
+  detector_url: http://localhost:5002
+`);
+
+    try {
+      const config = loadConfig(path);
+
+      expect(config.pii_detection.phone_regions).toEqual([]);
+    } finally {
+      cleanupConfig(path);
+    }
+  });
+
   test("rejects invalid phone region codes", () => {
     const path = writeConfig(`
 mode: mask
index 2cc94160bcfd8ba0ce6887abbf8fc0d493880571..a999e5748e3eb3c7a791319e7330ab67bd5b8c1e 100644 (file)
@@ -113,26 +113,8 @@ const PhoneRegionsSchema = z
       .map((s) => s.trim())
       .filter(Boolean);
   })
-  .pipe(z.array(PhoneRegionSchema).min(1))
-  .default([
-    "US",
-    "GB",
-    "DE",
-    "AT",
-    "CH",
-    "IT",
-    "FR",
-    "BE",
-    "LU",
-    "ES",
-    "NL",
-    "PT",
-    "BR",
-    "PL",
-    "RO",
-    "MD",
-    "IN",
-  ]);
+  .pipe(z.array(PhoneRegionSchema))
+  .default([]);
 
 const PIIDetectionSchema = z.object({
   enabled: z.boolean().default(true),
index 476d969704300e3305f6e483dd15eacf91ccfb41..27be078225142909e5b965a308066e2308d40491 100644 (file)
@@ -166,7 +166,7 @@ Mode:       ${config.mode.toUpperCase()}
 ${modeInfo}
 
 PII Detection:
-  Phone regions: ${config.pii_detection.phone_regions.join(", ")}
+  Phone regions: ${config.pii_detection.phone_regions.length > 0 ? config.pii_detection.phone_regions.join(", ") : "none (+ international only)"}
   Threshold: ${config.pii_detection.score_threshold}
   Entities:  ${config.pii_detection.entities.join(", ")}
 
git clone https://git.99rst.org/PROJECT