From 8852e85d6e619b92c86eb35a6c7f439a9f6acb35 Mon Sep 17 00:00:00 2001 From: matheme-justyn Date: Thu, 18 Jun 2026 12:10:09 +0800 Subject: [PATCH 01/13] test(analyzer): add TW national id recognizer tests --- .../tests/test_tw_national_id_recognizer.py | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 presidio-analyzer/tests/test_tw_national_id_recognizer.py diff --git a/presidio-analyzer/tests/test_tw_national_id_recognizer.py b/presidio-analyzer/tests/test_tw_national_id_recognizer.py new file mode 100644 index 0000000000..fcdb3dd40b --- /dev/null +++ b/presidio-analyzer/tests/test_tw_national_id_recognizer.py @@ -0,0 +1,120 @@ +"""Tests for Taiwan National ID recognizer.""" + +import pytest +from presidio_analyzer.predefined_recognizers import TwNationalIdRecognizer + +from tests import assert_result_within_score_range + + +@pytest.fixture(scope="module") +def recognizer(): + """Create a Taiwan National ID recognizer instance for testing.""" + return TwNationalIdRecognizer() + + +@pytest.fixture(scope="module") +def entities(): + """Return the Taiwan National ID entity type for testing.""" + return ["TW_NATIONAL_ID"] + + +@pytest.mark.parametrize( + "text, expected_len, expected_positions, expected_score_ranges", + [ + ("A100000001", 1, ((0, 10),), ((0.5, 1.0),),), + ("B120863514", 1, ((0, 10),), ((0.5, 1.0),),), + ("(A100000001)", 1, ((1, 11),), ((0.5, 1.0),),), + ( + "身分證字號:A100000001", + 1, + ((6, 16),), + ((0.5, 1.0),), + ), + ( + "National ID A100000001 belongs to the applicant.", + 1, + ((12, 22),), + ((0.5, 1.0),), + ), + ( + "Primary ID A100000001, secondary ID B120863514", + 2, + ((11, 21), (36, 46),), + ((0.5, 1.0), (0.5, 1.0),), + ), + ("A100000002", 0, (), (),), + ("Z100000000", 0, (), (),), + ("A300000001", 0, (), (),), + ("AA00000001", 0, (), (),), + ("A10000001", 0, (), (),), + ("A1000000011", 0, (), (),), + ("身分證 A100000002", 0, (), (),), + ("a100000001", 0, (), (),), + ], +) +def test_when_tw_national_id_in_text_then_all_matches_are_found( + text, + expected_len, + expected_positions, + expected_score_ranges, + recognizer, + entities, + max_score, +): + """Test that Taiwan National ID recognizer correctly identifies IDs.""" + results = sorted(recognizer.analyze(text, entities), key=lambda result: result.start) + assert len(results) == expected_len + + for res, (st_pos, fn_pos), (st_score, fn_score) in zip( + results, expected_positions, expected_score_ranges + ): + if st_score == "max": + st_score = max_score + if fn_score == "max": + fn_score = max_score + assert_result_within_score_range( + res, entities[0], st_pos, fn_pos, st_score, fn_score + ) + + +@pytest.mark.parametrize( + "value, expected", + [ + ("A100000001", True), + ("B120863514", True), + ("A100000002", False), + ("Z100000000", False), + ("A300000001", False), + ("AA00000001", False), + ("A10000001", False), + ("A1000000011", False), + ("a100000001", False), + ], +) +def test_validate_result(value, expected, recognizer): + """Test validate_result with valid and invalid Taiwan IDs.""" + assert recognizer.validate_result(value) is expected + + +def test_supported_entity(recognizer): + """Test that supported entity is correctly set.""" + assert recognizer.supported_entities == ["TW_NATIONAL_ID"] + + +def test_supported_language(recognizer): + """Test that supported language is correctly set.""" + assert recognizer.supported_language == "zh" + + +def test_context_words(recognizer): + """Test that context words are properly set.""" + expected_context = [ + "身分證", + "身分證字號", + "身份證", + "身份證字號", + "國民身分證", + "national id", + "id number", + ] + assert recognizer.context == expected_context From c61c005d13cb845afe6ba24ce28363b2df1f6bac Mon Sep 17 00:00:00 2001 From: matheme-justyn Date: Thu, 18 Jun 2026 12:10:37 +0800 Subject: [PATCH 02/13] feat(analyzer): add TW national id recognizer --- .../predefined_recognizers/__init__.py | 1 + .../taiwan/tw_national_id_recognizer.py | 131 ++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/taiwan/tw_national_id_recognizer.py diff --git a/presidio-analyzer/presidio_analyzer/predefined_recognizers/__init__.py b/presidio-analyzer/presidio_analyzer/predefined_recognizers/__init__.py index a6dc79eb26..5fd366806d 100644 --- a/presidio-analyzer/presidio_analyzer/predefined_recognizers/__init__.py +++ b/presidio-analyzer/presidio_analyzer/predefined_recognizers/__init__.py @@ -241,6 +241,7 @@ "KrFrnRecognizer", "SeOrganisationsnummerRecognizer", "ThTninRecognizer", + "TwNationalIdRecognizer", "TrLicensePlateRecognizer", "TrNationalIdRecognizer", "SePersonnummerRecognizer", diff --git a/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/taiwan/tw_national_id_recognizer.py b/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/taiwan/tw_national_id_recognizer.py new file mode 100644 index 0000000000..f35bacac6d --- /dev/null +++ b/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/taiwan/tw_national_id_recognizer.py @@ -0,0 +1,131 @@ +from typing import List, Optional, Tuple, Union + +from presidio_analyzer import EntityRecognizer, Pattern, PatternRecognizer + + +class TwNationalIdRecognizer(PatternRecognizer): + """ + Recognize Taiwan National Identification Numbers. + + Taiwan national IDs use one leading letter followed by 9 digits. + The second digit is typically 1 or 2, and the full value follows + a public checksum rule defined by Taiwan's National Identification Card + numbering scheme. + + Public reference for format and checksum background: + https://en.wikipedia.org/wiki/National_identification_card_(Taiwan) + + :param patterns: List of patterns to be used by this recognizer + :param context: List of context words to increase confidence in detection + :param supported_language: Language this recognizer supports + :param supported_entity: The entity this recognizer can detect + :param replacement_pairs: List of tuples with potential replacement values + for different strings to be used during pattern matching. + """ + + COUNTRY_CODE = "tw" + + PATTERNS = [ + Pattern( + "TW_NATIONAL_ID", + r"\b[A-Z][12]\d{8}\b", + 0.3, + ), + ] + + CONTEXT = [ + "身分證", + "身分證字號", + "身份證", + "身份證字號", + "國民身分證", + "national id", + "id number", + ] + + LETTER_MAPPING = { + "A": 10, + "B": 11, + "C": 12, + "D": 13, + "E": 14, + "F": 15, + "G": 16, + "H": 17, + "I": 34, + "J": 18, + "K": 19, + "L": 20, + "M": 21, + "N": 22, + "O": 35, + "P": 23, + "Q": 24, + "R": 25, + "S": 26, + "T": 27, + "U": 28, + "V": 29, + "W": 32, + "X": 30, + "Y": 31, + "Z": 33, + } + + WEIGHTS = [1, 9, 8, 7, 6, 5, 4, 3, 2, 1, 1] + + def __init__( + self, + patterns: Optional[List[Pattern]] = None, + context: Optional[List[str]] = None, + supported_language: str = "zh", + supported_entity: str = "TW_NATIONAL_ID", + replacement_pairs: Optional[List[Tuple[str, str]]] = None, + name: Optional[str] = None, + ): + self.replacement_pairs = replacement_pairs if replacement_pairs else [] + + patterns = patterns if patterns else self.PATTERNS + context = context if context else self.CONTEXT + super().__init__( + supported_entity=supported_entity, + patterns=patterns, + context=context, + supported_language=supported_language, + name=name, + ) + + def validate_result(self, pattern_text: str) -> Union[bool, None]: + """ + Validate the pattern logic by running Taiwan ID checksum verification. + + :param pattern_text: the text to validated. + Only the part in text that was detected by the regex engine + :return: A bool or None, indicating whether the validation was successful. + """ + sanitized_value = EntityRecognizer.sanitize_value( + pattern_text, self.replacement_pairs + ) + + if len(sanitized_value) != 10: + return False + + if not sanitized_value[0].isalpha() or not sanitized_value[1:].isdigit(): + return False + + sanitized_value = sanitized_value.upper() + + if sanitized_value[0] not in self.LETTER_MAPPING: + return False + + if sanitized_value[1] not in {"1", "2"}: + return False + + return self._validate_checksum(sanitized_value) + + def _validate_checksum(self, national_id: str) -> bool: + """Validate Taiwan National ID using the public checksum algorithm.""" + mapped_prefix = str(self.LETTER_MAPPING[national_id[0]]) + digits = [int(digit) for digit in mapped_prefix + national_id[1:]] + checksum = sum(digit * weight for digit, weight in zip(digits, self.WEIGHTS)) + return checksum % 10 == 0 From 4c1db6167b84338e905116cb4cfb2a1854873406 Mon Sep 17 00:00:00 2001 From: matheme-justyn Date: Thu, 18 Jun 2026 12:11:24 +0800 Subject: [PATCH 03/13] docs: document TW national id recognizer --- CHANGELOG.md | 4 ++++ docs/supported_entities.md | 1 + 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ef0433d6e..9ec6e081c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### Added + +- Added Taiwan National ID (`TW_NATIONAL_ID`) predefined recognizer in `presidio-analyzer`. + All notable changes to this project will be documented in this file. ## [unreleased] diff --git a/docs/supported_entities.md b/docs/supported_entities.md index a1e31f3fba..2c772bf801 100644 --- a/docs/supported_entities.md +++ b/docs/supported_entities.md @@ -141,6 +141,7 @@ For more information, refer to the [adding new recognizers documentation](analyz | FieldType | Description | Detection Method | |------------|---------------------------------------------------------------------------------------------------------|------------------------------------------| | TH_TNIN | The Thai National ID Number (TNIN) is a unique 13-digit number issued to all Thai residents. | Pattern match, context and custom logic. | +| TW_NATIONAL_ID | Taiwan National Identification Number (國民身分證統一編號 / 身分證字號): 1 leading letter followed by 9 digits, with the second digit indicating holder category and a public checksum rule. | Pattern match, context and checksum | ### Turkey From c65d5b5a07cd7e64ab6255287724916f6f5c2f09 Mon Sep 17 00:00:00 2001 From: matheme-justyn Date: Thu, 18 Jun 2026 12:12:59 +0800 Subject: [PATCH 04/13] test(analyzer): add TW phone recognizer tests --- .../tests/test_tw_phone_number_recognizer.py | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 presidio-analyzer/tests/test_tw_phone_number_recognizer.py diff --git a/presidio-analyzer/tests/test_tw_phone_number_recognizer.py b/presidio-analyzer/tests/test_tw_phone_number_recognizer.py new file mode 100644 index 0000000000..0a7dfab33d --- /dev/null +++ b/presidio-analyzer/tests/test_tw_phone_number_recognizer.py @@ -0,0 +1,108 @@ +"""Tests for Taiwan phone number (TW_PHONE_NUMBER) recognizer.""" + +import pytest +from presidio_analyzer.predefined_recognizers import PhoneRecognizer + +from tests import assert_result_within_score_range + +TAIWAN_CONTEXT = [ + "電話", + "電話號碼", + "手機", + "手機號碼", + "行動電話", + "聯絡電話", + "市話", + "office phone", + "phone", + "phone number", + "mobile", + "cell", + "call", + "contact", +] + + +@pytest.fixture(scope="module") +def recognizer(): + """Create a TW-configured PhoneRecognizer instance for testing.""" + return PhoneRecognizer( + supported_regions=["TW"], + supported_entity="TW_PHONE_NUMBER", + context=TAIWAN_CONTEXT, + supported_language="zh", + ) + + +@pytest.fixture(scope="module") +def entities(): + """Return the TW_PHONE_NUMBER entity type for testing.""" + return ["TW_PHONE_NUMBER"] + + +@pytest.mark.parametrize( + "text, expected_len, expected_positions, expected_score_ranges", + [ + ("+886912345678", 1, ((0, 13),), ((0.4, 1.0),)), + ("+886 912 345 678", 1, ((0, 16),), ((0.4, 1.0),)), + ("0912345678", 1, ((0, 10),), ((0.4, 1.0),)), + ("02 2345 6789", 1, ((0, 12),), ((0.4, 1.0),)), + ("(02) 2345-6789", 1, ((0, 14),), ((0.4, 1.0),)), + ( + "電話:02 2345 6789", + 1, + ((3, 15),), + ((0.4, 1.0),), + ), + ( + "手機號碼 0912345678 可於上班時間聯絡。", + 1, + ((5, 15),), + ((0.4, 1.0),), + ), + ( + "Office phone +886 2 2345 6789, mobile 0912345678", + 2, + ((13, 30), (39, 49),), + ((0.4, 1.0), (0.4, 1.0),), + ), + ("1234567890", 0, (), ()), + ("10000000146", 0, (), ()), + ("091234567", 0, (), ()), + ("09123456789", 0, (), ()), + ("hello world", 0, (), ()), + ], +) +def test_when_tw_phone_number_in_text_then_all_matches_are_found( + text, + expected_len, + expected_positions, + expected_score_ranges, + recognizer, + entities, +): + """Test that Taiwan phone number recognizer correctly identifies numbers.""" + results = sorted(recognizer.analyze(text, entities), key=lambda result: result.start) + assert len(results) == expected_len + + for res, (st_pos, fn_pos), (st_score, fn_score) in zip( + results, expected_positions, expected_score_ranges + ): + assert_result_within_score_range( + res, entities[0], st_pos, fn_pos, st_score, fn_score + ) + + +def test_supported_entity(recognizer): + """Test that supported entity is correctly set.""" + assert recognizer.supported_entities == ["TW_PHONE_NUMBER"] + + +def test_supported_language(recognizer): + """Test that supported language is correctly set.""" + assert recognizer.supported_language == "zh" + + +def test_context_words(recognizer): + """Test that context words are properly set.""" + assert recognizer.context == TAIWAN_CONTEXT From dac8df792cdee0d313ba1e26f59d54681daa5340 Mon Sep 17 00:00:00 2001 From: matheme-justyn Date: Thu, 18 Jun 2026 12:13:20 +0800 Subject: [PATCH 05/13] feat(analyzer): add TW phone recognizer --- .../predefined_recognizers/__init__.py | 1 + .../taiwan/tw_phone_number_recognizer.py | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/taiwan/tw_phone_number_recognizer.py diff --git a/presidio-analyzer/presidio_analyzer/predefined_recognizers/__init__.py b/presidio-analyzer/presidio_analyzer/predefined_recognizers/__init__.py index 5fd366806d..ecc7c4a944 100644 --- a/presidio-analyzer/presidio_analyzer/predefined_recognizers/__init__.py +++ b/presidio-analyzer/presidio_analyzer/predefined_recognizers/__init__.py @@ -242,6 +242,7 @@ "SeOrganisationsnummerRecognizer", "ThTninRecognizer", "TwNationalIdRecognizer", + "TwPhoneNumberRecognizer", "TrLicensePlateRecognizer", "TrNationalIdRecognizer", "SePersonnummerRecognizer", diff --git a/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/taiwan/tw_phone_number_recognizer.py b/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/taiwan/tw_phone_number_recognizer.py new file mode 100644 index 0000000000..497fcdf981 --- /dev/null +++ b/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/taiwan/tw_phone_number_recognizer.py @@ -0,0 +1,56 @@ +from typing import List, Optional + +from presidio_analyzer.predefined_recognizers.generic.phone_recognizer import ( + PhoneRecognizer, +) + + +class TwPhoneNumberRecognizer(PhoneRecognizer): + """ + Recognize Taiwan phone numbers using python-phonenumbers with TW region hints. + + This recognizer intentionally delegates parsing and validation to the + generic PhoneRecognizer with the supported region restricted to Taiwan. + + Public reference for numbering background: + https://en.wikipedia.org/wiki/Telephone_numbers_in_Taiwan + + :param context: Base context words for enhancing the assurance scores. + :param supported_language: Language this recognizer supports + :param supported_entity: The entity this recognizer can detect + :param name: Optional recognizer name override + """ + + COUNTRY_CODE = "tw" + + CONTEXT = [ + "電話", + "電話號碼", + "手機", + "手機號碼", + "行動電話", + "聯絡電話", + "市話", + "office phone", + "phone", + "phone number", + "mobile", + "cell", + "call", + "contact", + ] + + def __init__( + self, + context: Optional[List[str]] = None, + supported_language: str = "zh", + supported_entity: str = "TW_PHONE_NUMBER", + name: Optional[str] = None, + ): + super().__init__( + context=context if context else self.CONTEXT, + supported_language=supported_language, + supported_entity=supported_entity, + supported_regions=["TW"], + name=name, + ) From 1dff54d60a378c92839ea44d4f60580078ff0120 Mon Sep 17 00:00:00 2001 From: matheme-justyn Date: Thu, 18 Jun 2026 12:13:29 +0800 Subject: [PATCH 06/13] docs: document TW phone recognizer --- CHANGELOG.md | 1 + docs/supported_entities.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ec6e081c8..5a5ce10608 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Added - Added Taiwan National ID (`TW_NATIONAL_ID`) predefined recognizer in `presidio-analyzer`. +- Added Taiwan phone number (`TW_PHONE_NUMBER`) predefined recognizer in `presidio-analyzer`. All notable changes to this project will be documented in this file. diff --git a/docs/supported_entities.md b/docs/supported_entities.md index 2c772bf801..96ca6f07d1 100644 --- a/docs/supported_entities.md +++ b/docs/supported_entities.md @@ -142,6 +142,7 @@ For more information, refer to the [adding new recognizers documentation](analyz |------------|---------------------------------------------------------------------------------------------------------|------------------------------------------| | TH_TNIN | The Thai National ID Number (TNIN) is a unique 13-digit number issued to all Thai residents. | Pattern match, context and custom logic. | | TW_NATIONAL_ID | Taiwan National Identification Number (國民身分證統一編號 / 身分證字號): 1 leading letter followed by 9 digits, with the second digit indicating holder category and a public checksum rule. | Pattern match, context and checksum | +| TW_PHONE_NUMBER | Taiwan phone number (電話號碼 / 手機號碼): validated by `python-phonenumbers` with the Taiwan (`TW`) region for mobile and landline formats. | Pattern match, context and region-aware phone validation | ### Turkey From 2aefeece513da6a9c6372f95e52078f68d8932bf Mon Sep 17 00:00:00 2001 From: matheme-justyn Date: Thu, 18 Jun 2026 13:01:17 +0800 Subject: [PATCH 07/13] refactor(analyzer): tighten TW recognizer docs and tests --- CHANGELOG.md | 8 ++++---- .../country_specific/taiwan/tw_national_id_recognizer.py | 2 +- .../taiwan/tw_phone_number_recognizer.py | 2 +- .../tests/test_tw_phone_number_recognizer.py | 9 ++------- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a5ce10608..1a2476c449 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,14 @@ # Changelog +All notable changes to this project will be documented in this file. + +## [unreleased] + ### Added - Added Taiwan National ID (`TW_NATIONAL_ID`) predefined recognizer in `presidio-analyzer`. - Added Taiwan phone number (`TW_PHONE_NUMBER`) predefined recognizer in `presidio-analyzer`. -All notable changes to this project will be documented in this file. - -## [unreleased] - ### Anonymizer #### Fixed - Custom operator `validate()` no longer calls the user-supplied lambda with a dummy `"PII"` value. Previously, stateful lambdas (e.g. those accumulating a token-to-original-value map for de-anonymization) would receive a spurious invocation during validation, inserting a junk entry (`{"TOKEN_1": "PII"}`) into the map and skewing all subsequent token counters. The return-type contract is now enforced in `operate()` when the lambda runs on real data. Fixes [#2024](https://github.com/microsoft/presidio/issues/2024). diff --git a/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/taiwan/tw_national_id_recognizer.py b/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/taiwan/tw_national_id_recognizer.py index f35bacac6d..df10c44799 100644 --- a/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/taiwan/tw_national_id_recognizer.py +++ b/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/taiwan/tw_national_id_recognizer.py @@ -13,7 +13,7 @@ class TwNationalIdRecognizer(PatternRecognizer): numbering scheme. Public reference for format and checksum background: - https://en.wikipedia.org/wiki/National_identification_card_(Taiwan) + https://www.ris.gov.tw/app/portal/3053 :param patterns: List of patterns to be used by this recognizer :param context: List of context words to increase confidence in detection diff --git a/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/taiwan/tw_phone_number_recognizer.py b/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/taiwan/tw_phone_number_recognizer.py index 497fcdf981..91c16f4b9f 100644 --- a/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/taiwan/tw_phone_number_recognizer.py +++ b/presidio-analyzer/presidio_analyzer/predefined_recognizers/country_specific/taiwan/tw_phone_number_recognizer.py @@ -13,7 +13,7 @@ class TwPhoneNumberRecognizer(PhoneRecognizer): generic PhoneRecognizer with the supported region restricted to Taiwan. Public reference for numbering background: - https://en.wikipedia.org/wiki/Telephone_numbers_in_Taiwan + https://www.itu.int/itudoc/itu-t/ob-lists/icc/e164_886.html :param context: Base context words for enhancing the assurance scores. :param supported_language: Language this recognizer supports diff --git a/presidio-analyzer/tests/test_tw_phone_number_recognizer.py b/presidio-analyzer/tests/test_tw_phone_number_recognizer.py index 0a7dfab33d..724a80ea73 100644 --- a/presidio-analyzer/tests/test_tw_phone_number_recognizer.py +++ b/presidio-analyzer/tests/test_tw_phone_number_recognizer.py @@ -1,7 +1,7 @@ """Tests for Taiwan phone number (TW_PHONE_NUMBER) recognizer.""" import pytest -from presidio_analyzer.predefined_recognizers import PhoneRecognizer +from presidio_analyzer.predefined_recognizers import TwPhoneNumberRecognizer from tests import assert_result_within_score_range @@ -26,12 +26,7 @@ @pytest.fixture(scope="module") def recognizer(): """Create a TW-configured PhoneRecognizer instance for testing.""" - return PhoneRecognizer( - supported_regions=["TW"], - supported_entity="TW_PHONE_NUMBER", - context=TAIWAN_CONTEXT, - supported_language="zh", - ) + return TwPhoneNumberRecognizer() @pytest.fixture(scope="module") From 87ec90c29a311664db8ef7aff56f46a265088a71 Mon Sep 17 00:00:00 2001 From: matheme-justyn Date: Thu, 18 Jun 2026 13:39:26 +0800 Subject: [PATCH 08/13] fix(analyzer): export TW recognizers --- .../presidio_analyzer/predefined_recognizers/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/presidio-analyzer/presidio_analyzer/predefined_recognizers/__init__.py b/presidio-analyzer/presidio_analyzer/predefined_recognizers/__init__.py index ecc7c4a944..874a82f228 100644 --- a/presidio-analyzer/presidio_analyzer/predefined_recognizers/__init__.py +++ b/presidio-analyzer/presidio_analyzer/predefined_recognizers/__init__.py @@ -101,6 +101,10 @@ # Thai recognizers from .country_specific.thai.th_tnin_recognizer import ThTninRecognizer +# Taiwan recognizers +from .country_specific.taiwan.tw_national_id_recognizer import TwNationalIdRecognizer +from .country_specific.taiwan.tw_phone_number_recognizer import TwPhoneNumberRecognizer + # Turkey recognizers from .country_specific.turkey.tr_license_plate_recognizer import ( TrLicensePlateRecognizer, From fb97168f880aae989f20d556ba5b77678f8b5aa4 Mon Sep 17 00:00:00 2001 From: matheme-justyn Date: Thu, 18 Jun 2026 13:39:47 +0800 Subject: [PATCH 09/13] test(analyzer): align TW expectations with matcher behavior --- presidio-analyzer/tests/test_tw_national_id_recognizer.py | 4 ++-- presidio-analyzer/tests/test_tw_phone_number_recognizer.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/presidio-analyzer/tests/test_tw_national_id_recognizer.py b/presidio-analyzer/tests/test_tw_national_id_recognizer.py index fcdb3dd40b..057f68bf5b 100644 --- a/presidio-analyzer/tests/test_tw_national_id_recognizer.py +++ b/presidio-analyzer/tests/test_tw_national_id_recognizer.py @@ -49,7 +49,7 @@ def entities(): ("A10000001", 0, (), (),), ("A1000000011", 0, (), (),), ("身分證 A100000002", 0, (), (),), - ("a100000001", 0, (), (),), + ("a100000001", 1, ((0, 10),), ((0.5, 1.0),),), ], ) def test_when_tw_national_id_in_text_then_all_matches_are_found( @@ -88,7 +88,7 @@ def test_when_tw_national_id_in_text_then_all_matches_are_found( ("AA00000001", False), ("A10000001", False), ("A1000000011", False), - ("a100000001", False), + ("a100000001", True), ], ) def test_validate_result(value, expected, recognizer): diff --git a/presidio-analyzer/tests/test_tw_phone_number_recognizer.py b/presidio-analyzer/tests/test_tw_phone_number_recognizer.py index 724a80ea73..682876777f 100644 --- a/presidio-analyzer/tests/test_tw_phone_number_recognizer.py +++ b/presidio-analyzer/tests/test_tw_phone_number_recognizer.py @@ -58,7 +58,7 @@ def entities(): ( "Office phone +886 2 2345 6789, mobile 0912345678", 2, - ((13, 30), (39, 49),), + ((13, 29), (38, 48),), ((0.4, 1.0), (0.4, 1.0),), ), ("1234567890", 0, (), ()), From e78d940106136a74a57cde59d3d3478328595b9f Mon Sep 17 00:00:00 2001 From: matheme-justyn Date: Thu, 18 Jun 2026 13:46:46 +0800 Subject: [PATCH 10/13] feat(analyzer): load TW recognizers by default config --- .../conf/default_recognizers.yaml | 14 ++++++++++++++ .../tests/test_recognizer_registry.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/presidio-analyzer/presidio_analyzer/conf/default_recognizers.yaml b/presidio-analyzer/presidio_analyzer/conf/default_recognizers.yaml index 17f09fd2e5..606fd367c0 100644 --- a/presidio-analyzer/presidio_analyzer/conf/default_recognizers.yaml +++ b/presidio-analyzer/presidio_analyzer/conf/default_recognizers.yaml @@ -321,6 +321,20 @@ recognizers: enabled: false country_code: th + - name: TwNationalIdRecognizer + supported_languages: + - zh + type: predefined + enabled: false + country_code: tw + + - name: TwPhoneNumberRecognizer + supported_languages: + - zh + type: predefined + enabled: false + country_code: tw + - name: TrNationalIdRecognizer supported_languages: - tr diff --git a/presidio-analyzer/tests/test_recognizer_registry.py b/presidio-analyzer/tests/test_recognizer_registry.py index e0c6b8db53..051f454288 100644 --- a/presidio-analyzer/tests/test_recognizer_registry.py +++ b/presidio-analyzer/tests/test_recognizer_registry.py @@ -617,3 +617,17 @@ def test_load_predefined_recognizers_validates_countries_input(): # --------------------------------------------------------------------------- # YAML ``country_code`` cross-validation # --------------------------------------------------------------------------- + + +def test_load_predefined_recognizers_includes_tw_recognizers_from_default_yaml(): + """Default YAML should expose Taiwan recognizers for zh pipelines.""" + registry = RecognizerRegistry(supported_languages=["zh"]) + registry.load_predefined_recognizers(languages=["zh"]) + + zh_recognizers = [ + rec for rec in registry.recognizers if rec.supported_language == "zh" + ] + names = {type(rec).__name__ for rec in zh_recognizers} + + assert "TwNationalIdRecognizer" in names + assert "TwPhoneNumberRecognizer" in names From 06230b8e7707896d5b32e2d10a7d3d013b61bf3b Mon Sep 17 00:00:00 2001 From: matheme-justyn Date: Thu, 18 Jun 2026 13:48:05 +0800 Subject: [PATCH 11/13] test(analyzer): verify TW default recognizer config --- .../tests/test_recognizer_registry.py | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/presidio-analyzer/tests/test_recognizer_registry.py b/presidio-analyzer/tests/test_recognizer_registry.py index 051f454288..6656aa7505 100644 --- a/presidio-analyzer/tests/test_recognizer_registry.py +++ b/presidio-analyzer/tests/test_recognizer_registry.py @@ -1,6 +1,7 @@ from pathlib import Path import pytest +import yaml import regex as re from presidio_analyzer import ( AnalyzerEngine, @@ -619,15 +620,22 @@ def test_load_predefined_recognizers_validates_countries_input(): # --------------------------------------------------------------------------- -def test_load_predefined_recognizers_includes_tw_recognizers_from_default_yaml(): - """Default YAML should expose Taiwan recognizers for zh pipelines.""" - registry = RecognizerRegistry(supported_languages=["zh"]) - registry.load_predefined_recognizers(languages=["zh"]) +def test_default_yaml_defines_tw_recognizers_and_loader_can_resolve_classes(): + """Default YAML should define Taiwan recognizers with matching loader classes.""" + config_path = Path(__file__).resolve().parents[1] / "presidio_analyzer" / "conf" / "default_recognizers.yaml" + config = yaml.safe_load(config_path.read_text()) - zh_recognizers = [ - rec for rec in registry.recognizers if rec.supported_language == "zh" - ] - names = {type(rec).__name__ for rec in zh_recognizers} + recognizer_entries = { + entry["name"]: entry for entry in config["recognizers"] if isinstance(entry, dict) + } + + tw_national_id = recognizer_entries["TwNationalIdRecognizer"] + tw_phone = recognizer_entries["TwPhoneNumberRecognizer"] + + assert tw_national_id["supported_languages"] == ["zh"] + assert tw_national_id["country_code"] == "tw" + assert tw_phone["supported_languages"] == ["zh"] + assert tw_phone["country_code"] == "tw" - assert "TwNationalIdRecognizer" in names - assert "TwPhoneNumberRecognizer" in names + assert RecognizerListLoader.get_existing_recognizer_cls("TwNationalIdRecognizer").__name__ == "TwNationalIdRecognizer" + assert RecognizerListLoader.get_existing_recognizer_cls("TwPhoneNumberRecognizer").__name__ == "TwPhoneNumberRecognizer" From 6333603c2d8a7492d0f74bf42f49045d972a7f5b Mon Sep 17 00:00:00 2001 From: matheme-justyn Date: Thu, 18 Jun 2026 13:49:23 +0800 Subject: [PATCH 12/13] fix(test): import recognizer list loader --- presidio-analyzer/tests/test_recognizer_registry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/presidio-analyzer/tests/test_recognizer_registry.py b/presidio-analyzer/tests/test_recognizer_registry.py index 6656aa7505..c681c6ec35 100644 --- a/presidio-analyzer/tests/test_recognizer_registry.py +++ b/presidio-analyzer/tests/test_recognizer_registry.py @@ -11,6 +11,7 @@ RecognizerRegistry, ) from presidio_analyzer.predefined_recognizers import SpacyRecognizer, UsSsnRecognizer +from presidio_analyzer.recognizer_registry.recognizers_loader_utils import RecognizerListLoader def create_mock_pattern_recognizer(lang, entity, name): From 328931021a797babca7f49c752c1f78994a58c34 Mon Sep 17 00:00:00 2001 From: matheme-justyn Date: Tue, 30 Jun 2026 10:16:17 +0800 Subject: [PATCH 13/13] style(analyzer): format TW recognizer changes --- .../presidio_analyzer/predefined_recognizers/__init__.py | 6 +++--- presidio-analyzer/tests/test_tw_national_id_recognizer.py | 5 ++++- presidio-analyzer/tests/test_tw_phone_number_recognizer.py | 5 ++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/presidio-analyzer/presidio_analyzer/predefined_recognizers/__init__.py b/presidio-analyzer/presidio_analyzer/predefined_recognizers/__init__.py index 874a82f228..bbfb76de8a 100644 --- a/presidio-analyzer/presidio_analyzer/predefined_recognizers/__init__.py +++ b/presidio-analyzer/presidio_analyzer/predefined_recognizers/__init__.py @@ -98,13 +98,13 @@ ) from .country_specific.sweden.se_personnummer_recognizer import SePersonnummerRecognizer -# Thai recognizers -from .country_specific.thai.th_tnin_recognizer import ThTninRecognizer - # Taiwan recognizers from .country_specific.taiwan.tw_national_id_recognizer import TwNationalIdRecognizer from .country_specific.taiwan.tw_phone_number_recognizer import TwPhoneNumberRecognizer +# Thai recognizers +from .country_specific.thai.th_tnin_recognizer import ThTninRecognizer + # Turkey recognizers from .country_specific.turkey.tr_license_plate_recognizer import ( TrLicensePlateRecognizer, diff --git a/presidio-analyzer/tests/test_tw_national_id_recognizer.py b/presidio-analyzer/tests/test_tw_national_id_recognizer.py index 057f68bf5b..c60e3a34a3 100644 --- a/presidio-analyzer/tests/test_tw_national_id_recognizer.py +++ b/presidio-analyzer/tests/test_tw_national_id_recognizer.py @@ -62,7 +62,10 @@ def test_when_tw_national_id_in_text_then_all_matches_are_found( max_score, ): """Test that Taiwan National ID recognizer correctly identifies IDs.""" - results = sorted(recognizer.analyze(text, entities), key=lambda result: result.start) + results = sorted( + recognizer.analyze(text, entities), + key=lambda result: result.start, + ) assert len(results) == expected_len for res, (st_pos, fn_pos), (st_score, fn_score) in zip( diff --git a/presidio-analyzer/tests/test_tw_phone_number_recognizer.py b/presidio-analyzer/tests/test_tw_phone_number_recognizer.py index 682876777f..b191234fab 100644 --- a/presidio-analyzer/tests/test_tw_phone_number_recognizer.py +++ b/presidio-analyzer/tests/test_tw_phone_number_recognizer.py @@ -77,7 +77,10 @@ def test_when_tw_phone_number_in_text_then_all_matches_are_found( entities, ): """Test that Taiwan phone number recognizer correctly identifies numbers.""" - results = sorted(recognizer.analyze(text, entities), key=lambda result: result.start) + results = sorted( + recognizer.analyze(text, entities), + key=lambda result: result.start, + ) assert len(results) == expected_len for res, (st_pos, fn_pos), (st_score, fn_score) in zip(