Skip to content
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.

### Analyzer
#### Added
- South African ID number (`ZA_ID_NUMBER`) recognizer for the 13-digit national identity number, using pattern matching, context words, birth-date validation, and Luhn checksum validation. Disabled by default.
- South African recognizers for `ZA_PASSPORT`, `ZA_INCOME_TAX_NUMBER`, `ZA_DRIVER_LICENSE`, `ZA_VAT_NUMBER`, `ZA_COMPANY_REGISTRATION`, `ZA_TRAFFIC_REGISTER_NUMBER`, `ZA_LICENSE_PLATE`, `ZA_MOBILE_NUMBER`, and `ZA_TELEPHONE_NUMBER`. All disabled by default.
- Added `NoOpNlpEngine` for configurations that do not require NLP engine artifacts, enabling standalone recognizers such as `HuggingFaceNerRecognizer` to run without a spaCy or Stanza model (#2071) (Thanks @ultramancode)
- Added per-recognizer and per-entity score threshold configuration in the recognizer registry YAML, with the analyzer's global `default_score_threshold` as the fallback (#2116) (Thanks @rodboev)
- Added `PhUmidRecognizer` for Philippine Unified Multi-Purpose ID (UMID/CRN) numbers in dashed and plain 12-digit formats; disabled by default (#2045) (Thanks @Surya-5555)
Expand Down
9 changes: 9 additions & 0 deletions docs/supported_entities.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,15 @@ For more information, refer to the [adding new recognizers documentation](analyz
| FieldType | Description | Detection Method |
|------------|---------------------------------------------------------------------------------------------------------|------------------------------------------|
| ZA_ID_NUMBER | The South African identity number is a 13-digit identifier in the `YYMMDDSSSSCAZ` format, where the trailing digit is validated with the Luhn algorithm. | Pattern match, context, and checksum. |
| ZA_PASSPORT | The South African passport number is a 9-character identifier with prefix letter A, D, M, or T followed by 8 digits. | Pattern match, context, and validation. |
| ZA_INCOME_TAX_NUMBER | The South African SARS income tax reference number is a 10-digit numeric identifier, commonly starting with 0, 1, 2, 3, or 9. | Pattern match, context, and validation. |
| ZA_DRIVER_LICENSE | The South African eNaTIS driver's licence number is an alphanumeric identifier of 10–14 characters. | Pattern match, context, and validation. |
| ZA_VAT_NUMBER | The South African VAT registration number is a 10-digit identifier starting with 4. | Pattern match, context, and validation. |
| ZA_COMPANY_REGISTRATION | The South African CIPC company registration number uses modern `YYYY/NNNNNN/NN` format or legacy prefixed formats such as CK. | Pattern match, context, and validation. |
| ZA_TRAFFIC_REGISTER_NUMBER | The South African eNaTIS traffic register number is a 13-digit identifier for foreigners and organisations, disambiguated from ZA_ID_NUMBER via validation. | Pattern match, context, and validation. |
| ZA_LICENSE_PLATE | The South African vehicle licence plate uses provincial formats such as compact suffix forms (e.g. KD93GKGP) or spaced layouts (e.g. DK 28 LF GP). | Pattern match, context, and validation. |
| ZA_MOBILE_NUMBER | The South African mobile (cellular) number uses a 9-digit national significant number with country code +27 or domestic trunk prefix 0 (primarily 06x, 07x, and cellular 08x ranges). | `phonenumbers` match, line-type filter, and context. |
| ZA_TELEPHONE_NUMBER | The South African telephone number covers geographic landlines (01x–05x) and non-mobile service lines such as toll-free (080), sharecall (086), and VoIP (087). | `phonenumbers` match, line-type filter, and context. |

### Thai
| FieldType | Description | Detection Method |
Expand Down
63 changes: 63 additions & 0 deletions presidio-analyzer/presidio_analyzer/conf/default_recognizers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -318,13 +318,76 @@ recognizers:
enabled: false
country_code: se

- name: ZaCompanyRegistrationRecognizer
supported_languages:
- en
type: predefined
enabled: false
country_code: za

- name: ZaDriverLicenseRecognizer
supported_languages:
- en
type: predefined
enabled: false
country_code: za

- name: ZaIdNumberRecognizer
supported_languages:
- en
type: predefined
enabled: false
country_code: za

- name: ZaIncomeTaxNumberRecognizer
supported_languages:
- en
type: predefined
enabled: false
country_code: za

- name: ZaLicensePlateRecognizer
supported_languages:
- en
type: predefined
enabled: false
country_code: za

- name: ZaMobileNumberRecognizer
supported_languages:
- en
type: predefined
enabled: false
country_code: za

- name: ZaPassportRecognizer
supported_languages:
- en
type: predefined
enabled: false
country_code: za

- name: ZaTrafficRegisterNumberRecognizer
supported_languages:
- en
type: predefined
enabled: false
country_code: za

- name: ZaTelephoneNumberRecognizer
supported_languages:
- en
type: predefined
enabled: false
country_code: za

- name: ZaVatNumberRecognizer
supported_languages:
- en
type: predefined
enabled: false
country_code: za

- name: ThTninRecognizer
supported_languages:
- th
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,34 @@
from .country_specific.singapore.sg_uen_recognizer import SgUenRecognizer

# South Africa recognizers
from .country_specific.south_africa.za_company_registration_recognizer import (
ZaCompanyRegistrationRecognizer,
)
from .country_specific.south_africa.za_driver_license_recognizer import (
ZaDriverLicenseRecognizer,
)
from .country_specific.south_africa.za_id_number_recognizer import (
ZaIdNumberRecognizer,
)
from .country_specific.south_africa.za_income_tax_number_recognizer import (
ZaIncomeTaxNumberRecognizer,
)
from .country_specific.south_africa.za_license_plate_recognizer import (
ZaLicensePlateRecognizer,
)
from .country_specific.south_africa.za_passport_recognizer import (
ZaPassportRecognizer,
)
from .country_specific.south_africa.za_phone_number_recognizer import (
ZaMobileNumberRecognizer,
ZaTelephoneNumberRecognizer,
)
from .country_specific.south_africa.za_traffic_register_number_recognizer import (
ZaTrafficRegisterNumberRecognizer,
)
from .country_specific.south_africa.za_vat_number_recognizer import (
ZaVatNumberRecognizer,
)

# Spain recognizers
from .country_specific.spain.es_nie_recognizer import EsNieRecognizer
Expand Down Expand Up @@ -250,7 +275,16 @@
"TrLicensePlateRecognizer",
"TrNationalIdRecognizer",
"SePersonnummerRecognizer",
"ZaCompanyRegistrationRecognizer",
"ZaDriverLicenseRecognizer",
"ZaIdNumberRecognizer",
"ZaIncomeTaxNumberRecognizer",
"ZaLicensePlateRecognizer",
"ZaMobileNumberRecognizer",
"ZaPassportRecognizer",
"ZaTelephoneNumberRecognizer",
"ZaTrafficRegisterNumberRecognizer",
"ZaVatNumberRecognizer",
"LangExtractRecognizer",
"AzureOpenAILangExtractRecognizer",
"BasicLangExtractRecognizer",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,27 @@
"""South Africa-specific recognizers."""

from .za_company_registration_recognizer import ZaCompanyRegistrationRecognizer
from .za_driver_license_recognizer import ZaDriverLicenseRecognizer
from .za_id_number_recognizer import ZaIdNumberRecognizer
from .za_income_tax_number_recognizer import ZaIncomeTaxNumberRecognizer
from .za_license_plate_recognizer import ZaLicensePlateRecognizer
from .za_passport_recognizer import ZaPassportRecognizer
from .za_phone_number_recognizer import (
ZaMobileNumberRecognizer,
ZaTelephoneNumberRecognizer,
)
from .za_traffic_register_number_recognizer import ZaTrafficRegisterNumberRecognizer
from .za_vat_number_recognizer import ZaVatNumberRecognizer

__all__ = [
"ZaCompanyRegistrationRecognizer",
"ZaDriverLicenseRecognizer",
"ZaIdNumberRecognizer",
"ZaIncomeTaxNumberRecognizer",
"ZaLicensePlateRecognizer",
"ZaMobileNumberRecognizer",
"ZaPassportRecognizer",
"ZaTelephoneNumberRecognizer",
"ZaTrafficRegisterNumberRecognizer",
"ZaVatNumberRecognizer",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from datetime import date
from typing import List, Optional

from presidio_analyzer import Pattern, PatternRecognizer


class ZaCompanyRegistrationRecognizer(PatternRecognizer):
"""
Recognize South African company registration numbers (CIPC).

Modern private and public companies use ``YYYY/NNNNNN/NN`` (year,
sequence, company-type suffix). Legacy formats include prefixed codes
such as ``CK`` (close corporation) and other CIPC entity prefixes.

Reference:
https://support.tradeshield.ai/support/solutions/articles/153000256853-cipc-company-codes-types-status-the-complete-guide

: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
"""

COUNTRY_CODE = "za"

LEGACY_PREFIXES = frozenset({"CK", "K", "T", "W", "B", "M", "N", "NR"})

PATTERNS = [
Pattern(
"South African Company Registration (modern)",
r"\b(?:19|20)\d{2}/\d{6}/\d{2}\b",
0.4,
),
Pattern(
"South African Company Registration (legacy)",
r"\b(?:CK|K|T|W|B|M|N|NR)\d{4}/\d{6}\b",
0.3,
),
Comment thread
thatomokoena marked this conversation as resolved.
]

CONTEXT = [
"cipc",
"company registration",
"registration number",
"close corporation",
"company reg",
"enterprise number",
]

def __init__(
self,
patterns: Optional[List[Pattern]] = None,
context: Optional[List[str]] = None,
supported_language: str = "en",
supported_entity: str = "ZA_COMPANY_REGISTRATION",
name: Optional[str] = None,
):
patterns = self.PATTERNS if patterns is None else patterns
context = self.CONTEXT if context is None else context
super().__init__(
supported_entity=supported_entity,
patterns=patterns,
context=context,
supported_language=supported_language,
name=name,
)

def validate_result(self, pattern_text: str) -> bool: # noqa: D102
text = pattern_text.upper()
parts = text.split("/")
if len(parts) == 3 and parts[0].isdigit():
return self._validate_modern_format(text)
if len(parts) == 2:
return self._validate_legacy_format(text)
return False

def _validate_modern_format(self, text: str) -> bool:
parts = text.split("/")
if len(parts) != 3:
return False
year_part, sequence_part, type_part = parts
if not (
year_part.isdigit()
and sequence_part.isdigit()
and type_part.isdigit()
):
return False
if len(year_part) != 4 or len(sequence_part) != 6 or len(type_part) != 2:
return False
year = int(year_part)
return 1800 <= year <= date.today().year

def _validate_legacy_format(self, text: str) -> bool:
slash_index = text.index("/")
prefix = text[:slash_index]
sequence = text[slash_index + 1 :]
if not sequence.isdigit() or len(sequence) != 6:
return False
for legacy_prefix in sorted(self.LEGACY_PREFIXES, key=len, reverse=True):
if prefix.startswith(legacy_prefix):
year_part = prefix[len(legacy_prefix) :]
if len(year_part) == 4 and year_part.isdigit():
year = int(year_part)
return 1800 <= year <= date.today().year
return False
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import re
from typing import List, Optional

from presidio_analyzer import Pattern, PatternRecognizer


class ZaDriverLicenseRecognizer(PatternRecognizer):
"""
Recognize South African driver's licence numbers issued by eNaTIS.

eNaTIS licence numbers are alphanumeric strings of 10–14
characters combining digit blocks with trailing letter groups.

Reference:
https://github.com/ugommirikwe/sa-license-decoder/blob/master/SPEC.md

: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
"""

COUNTRY_CODE = "za"

MIN_LENGTH = 10
MAX_LENGTH = 14

PATTERNS = [
Pattern(
"South African Driver's Licence",
r"\b\d{6,10}[A-Z0-9]{2,5}\b",
0.3,
),
]
Comment thread
thatomokoena marked this conversation as resolved.

CONTEXT = [
"licence",
"license",
"driving licence",
"driving license",
"driver's licence",
"driver's license",
"drivers licence",
"drivers license",
"enatis",
"natis",
"licence number",
"license number",
]

def __init__(
self,
patterns: Optional[List[Pattern]] = None,
context: Optional[List[str]] = None,
supported_language: str = "en",
supported_entity: str = "ZA_DRIVER_LICENSE",
name: Optional[str] = None,
):
patterns = self.PATTERNS if patterns is None else patterns
context = self.CONTEXT if context is None else context
super().__init__(
supported_entity=supported_entity,
patterns=patterns,
context=context,
supported_language=supported_language,
name=name,
)

def validate_result(self, pattern_text: str) -> bool: # noqa: D102
text = pattern_text.upper()
if not self.MIN_LENGTH <= len(text) <= self.MAX_LENGTH:
return False
if re.fullmatch(r"\d{6,10}[A-Z0-9]{2,5}", text) is None:
return False
return bool(re.search(r"[A-Z]", text))
Loading
Loading