Home

/

Keep PII Out of Your LLM

/

Deterministic Detection

Deterministic Detection

Chapter 5
Part II
3
min read

Where rules genuinely win

Chapter 4 put regex at 0.171 average F1, last by a wide margin. That is a fair verdict on regex as a general-purpose PII detector and a misleading one as a verdict on rules.

Rules win in one specific place: identifiers with a checksum. Payment cards, ABA routing numbers, National Provider Identifiers, IBANs, ISBNs. These are not natural language. They are designed artefacts with a defined structure and a built-in integrity check, and the integrity check is the thing that turns a guess into near-certainty.

A regular expression that matches sixteen digits will match an order reference, a timestamp concatenation, a product SKU, and a phone number with the spaces stripped. The same expression plus a Luhn check will reject essentially all of those and accept payment cards. You have gone from a pattern match to a validated identification, and you did it in a hundredth of a millisecond.

That distinction is the whole chapter. Match, then validate. A pattern without a validator is a false-positive generator, and false positives are how you end up redacting the order number in an email about an order.

The performance argument

The numbers from the same benchmark:

ApproachMedian latencyThroughput
Regex + checksum0.1 ms3,861 texts/sec
Presidio15.1 ms63 texts/sec
Piiranha118.5 ms8.3 texts/sec
GLiNER v1161.3 ms5.9 texts/sec

Deterministic detection is free. Not cheap: free, relative to anything else in the request. It runs in-process, with no network call, no container, no GPU, and no failure mode other than your own code. That means you can run it everywhere, on every payload, without a budget conversation. Run it on prompts, on completions, on log lines at the sink, on tool-call arguments, on file metadata. It costs nothing to add another place.

The model-based layer cannot make that claim, which is why Chapter 13 puts these in different positions in the pipeline.

Luhn, in C#

The check digit algorithm behind payment cards, and behind several national identifiers.

public static bool IsLuhnValid(ReadOnlySpan<char> digits)
{
    int sum = 0;
    bool doubling = false;

    for (int i = digits.Length - 1; i >= 0; i--)
    {
        if (!char.IsDigit(digits[i])) return false;

        int d = digits[i] - '0';
        if (doubling)
        {
            d *= 2;
            if (d > 9) d -= 9;
        }

        sum += d;
        doubling = !doubling;
    }

    return digits.Length >= 12 && sum % 10 == 0;
}

Used properly, it sits behind a pattern rather than in front of it:

private static readonly Regex CardCandidate =
    new(@"\b(?:\d[ -]*?){12,19}\b", RegexOptions.Compiled);

public static IEnumerable<Match> FindPaymentCards(string text)
{
    foreach (Match m in CardCandidate.Matches(text))
    {
        var digits = new string(m.Value.Where(char.IsDigit).ToArray());
        if (IsLuhnValid(digits)) yield return m;
    }
}

The regex is deliberately loose because the validator is strict. That is the correct division of labour: cast wide, then verify. Inverting it, with a tight regex and no validator, gives you the worst of both.

ABA routing number, in C#

The nine-digit routing number on every US check and ACH transfer. Weights 3, 7, 1 repeating across the nine positions, and the weighted sum must be a multiple of ten.

private static readonly int[] AbaWeights = [3, 7, 1, 3, 7, 1, 3, 7, 1];

public static bool IsAbaRoutingValid(ReadOnlySpan<char> routing)
{
    if (routing.Length != 9) return false;

    int sum = 0;
    for (int i = 0; i < 9; i++)
    {
        if (!char.IsDigit(routing[i])) return false;
        sum += (routing[i] - '0') * AbaWeights[i];
    }

    return sum % 10 == 0;
}

Known-valid test values: 021000021, 011000015.

A routing number identifies a bank rather than a person, so on its own it is not personal data. Next to an account number it is, and the pair is what appears in the ACH details a customer pastes into a support ticket. Detect the routing number, then look for digits near it.

the-leak-you-cant-see
blast-radius
what-counts-as-pii
the-five-doors
the-accuracy-reckoning
the-hybrid-that-does-not-work
deterministic-detection
npi-in-c
the-three-way-choice
calling-the-analyzer-from-c
measuring-your-own-demo-gap
choosing-the-operating-point
the-ladder-of-safeguards
pseudonymisation
the-round-trip
restoring-safely
when-masking-breaks-the-task
plausibility-hazard
the-architecture-that-holds
the-reference-architecture
dont-send-it-at-all
structure-beats-prose
the-gateway
failure-is-a-policy-decision
the-sidecar-you-can-trust
egress-deny-it-at-the-network
rag-and-agents
de-identify-before-you-embed
dual-model-separation
the-boring-controls
evidence-and-the-first-thirty-days
week-two-the-chokepoint-and-the-fast-layer
entity-catalogue-and-c-validators
mbi-positional-rules
tooling-at-a-glance
azure-ai-language-pii-in-detail
container-trust-checklist
sources
azure-ai-language
provider-retention

Download the full PDF for free?

Free download — no account required

Get the PDF
Get the PDF
Related Chapters
Free Download
Get the full PDF
All pages, including all code examples, diagrams, and the appendix reference card.
No spam. Unsubscribe at any time.
Your email won't be shared.
Oops! There's a problem with your request. We're working on fixing it. Please try again later.