Cross-document match types
A cross-document check compares a shared fact across two or more documents in a run. The check's match type decides how those values are compared, what a Pass means, and when the case falls through to a human.
For configuration, see Cross-Document Checks. For the officer view of the results, see Checks → Cross-document checks.
At a glance¶
| Match type | Compares | Verdicts it can return | Best for |
|---|---|---|---|
| Exact | Normalised equality of the joined value | Pass, Mismatch, Insufficient data | IDs, VAT/PE numbers, emails, phones, dates |
| Fuzzy | Token-based similarity with an AI grey band | Pass, Mismatch, Needs review, Insufficient data | Names, wording, free-text descriptions |
| Range (period covers) | Whether candidate periods fully cover a reference period | Pass, Mismatch, Needs review, Insufficient data | Insurance, tenancy, or employment that must span a stay |
| Signature | Nothing automatically | Needs review | Signature-image comparison by a human |
| External system | Nothing automatically | Needs review | Verifications that require a third-party check |
All match types share three additional outcomes:
- Insufficient data when fewer than two documents carry the compared fields, or the reference side of a Range check is missing.
- Needs review when the computed result is not confident enough to stand on its own.
- Officer overrides ("mark as OK", "confirm mismatch", "not applicable") live alongside the computed result and are preserved through reprocessing while the computed status is unchanged.
Exact¶
Use Exact when both sides carry the same standardised identifier and any difference should be treated as a mismatch.
Before comparing, each value is normalised in a kind-aware way:
- Dates are parsed from common formats (
YYYY-MM-DD,DD/MM/YYYY,1 January 2025, ISO, etc.) and compared as ISO dates. - Emails are trimmed and casefolded.
- Identifiers (values with digits and optional separators such as spaces, hyphens, dots or slashes) are reduced to alphanumerics and casefolded. For all-digit values longer than eight characters, only the trailing eight digits are compared so that country-code prefixes on phone numbers do not cause false mismatches.
- Everything else is casefolded with whitespace collapsed.
If every document produces the same normalised value the check passes. Otherwise the raw values from each document are reported in the mismatch message.
Tip
Exact is the right choice for ID card numbers, passport numbers, VAT numbers, PE numbers, dates of birth, and email addresses. For anything that a human would still call "the same" despite a spelling variant, use Fuzzy.
Fuzzy¶
Use Fuzzy when the values should agree in meaning but the exact spelling or word order may vary — most commonly names, addresses, job titles, and free-text descriptions.
Values are compared pair-wise using
rapidfuzz token_set_ratio (0-100),
and the weakest pair decides the check:
| Similarity score | Result |
|---|---|
| ≥ 90 | Pass |
| 55 - 90 (grey band) | Ask the AI adjudicator, else Needs review |
| ≤ 55 | Mismatch, unless the AI adjudicator confidently disagrees |
The grey band exists because token order and initials — for example
P. Palackathadathil vs Palackathadathil P — legitimately land there.
Person names¶
If either side of the comparison is stored with the PersonName field type,
Identità applies name-aware rules on top of the fuzzy score:
- Honorifics (
Mr,Mrs,Dr,Prof,Eng,Rev,Hon,Sir, …) are stripped; - Punctuation is removed and all tokens are compared in upper case;
- Missing middle names are tolerated as long as every token on the shorter side matches a distinct token on the longer side; and
- A single-letter token stands for any longer token that starts with it, so
John P SmithmatchesJohn Palackathadathil Smith.
AI adjudication¶
When the score does not clear-pass, the check is handed to the
AdjudicatorAgent (see apps.zf_ai.agents.adjudicator). The agent returns
a verdict, a confidence, and a short reason. Two thresholds are involved:
the similarity score decides whether the agent is consulted, and the
agent's own confidence decides whether its answer is trusted.
In the grey band (55 - 90):
- If confidence < 70 %, the result is Needs review and the reason is attached to the check details.
- Otherwise the agent's Pass or Mismatch verdict stands and its reasoning is stored on the result.
- If the adjudicator cannot be reached (for example the model provider is unavailable), the check falls back to Needs review with the raw similarity scores in the details rather than guessing.
On a clear fail (≤ 55) the agent gets a chance to rescue wording variance that the lexical score cannot see — translations, synonyms, or a title buried in a list:
- A confident match verdict (confidence ≥ 70 %) turns the result into a Pass, with the reasoning stored on the result.
- A confident mismatch verdict keeps the Mismatch, now with the agent's reasoning attached.
- An unsure verdict or an unreachable adjudicator keeps the deterministic Mismatch — AI doubt never softens a clear lexical fail.
List fields and multi-value sources¶
When a field set consists of a single list-typed field (StringList,
JobTitleList, and their aliases), the extracted items are compared
individually and automatically — no configuration needed. Each item is
a candidate, and the best-scoring pair wins. This is what lets a job
advert whose job_titles list contains Network Security Engineer (among
four other roles) satisfy the single job_title on a work contract.
Because the raw list items are used directly, titles containing and,
/, or commas are never split apart.
For plain free-text fields that pack several values into one string,
mark the field set multi-value in the check configuration. The value
is then split on delimiters (,, ;, /, |, and, &, and common
bullet characters) and each part becomes a candidate.
When the AI adjudicator is consulted, list sides are presented to it as candidate sets with the same any-of semantics, so its verdict accounts for the fact that only one advertised title needs to match.
Range (period covers)¶
Use Range when one document defines a period that another document (or several) must fully cover. The most common example is a residence stay that must be covered end-to-end by health insurance, accommodation, or employment.
Configure exactly one field set as the reference — the period that must be covered. Every other field set contributes candidate periods that need to span it.
The engine reads each side's date fields with a light heuristic:
- names containing
from,start, orcommencementbecome the start; and - names containing
to,end,until, orexpirybecome the end; - otherwise the first parseable date is treated as the start and the next as the end;
- if start and end are the wrong way round they are swapped.
For every candidate document, coverage passes when
start ≤ reference_start and end ≥ reference_end.
| Situation | Result |
|---|---|
| Reference has no start or end that could be parsed | Needs review |
| A candidate has no parseable period | Needs review |
| A candidate period does not cover the reference | Mismatch |
| Every candidate covers the reference | Pass |
| No reference set, or no candidates present | Insufficient data |
The mismatch message names the offending documents and the periods they actually cover.
Signature¶
Signature comparison is not performed automatically. When the match type is Signature, the check always returns Needs review and the collected signature evidence is presented to the officer for a visual comparison. The values gathered from each document are still attached to the check details so the reviewer can jump straight to the relevant pages.
Use Signature when you want the check to appear on the Run Detail page for an officer to complete — for example, comparing the signature on a work contract against the signature on the ID card.
External system¶
External system works the same way as Signature in terms of the computed status: the check always resolves to Needs review so an officer can perform the third-party verification and record the outcome.
Pick this match type when the ground truth lives outside Identità — for example when a value must be looked up in a government registry, a police system, or another portal that the operator visits by hand.
Money (automatic sub-type)¶
Money is not a separate match type in the UI, but Identità automatically
switches to a money-aware comparison whenever either side of an Exact
or Fuzzy check uses a monetary field type (MoneyAmount, SalaryBand,
or PayPeriod). This makes wage comparisons between adverts, contracts,
and payslips work out of the box.
- Concrete amounts (
MoneyAmountand free-text numbers) are normalised to an annual figure using the pay period found in the value or hinted by the field name — recognised keywords includeannum,annual,yearly,p.a.,monthly,weekly,daily, andhourly. AMoneyAmountfield is typically paired with aPayPeriodsibling field in the same field set so the period travels with the amount. - Salary bands are parsed as either:
- the structured
SalaryBandfield type — a{currency, minimum, maximum}object — which yields an inclusive(minimum, maximum)range; or - a free-text value on a field whose name contains
band, in which case strings like€15,000 - €20,000,Under €15,000, andOver €30,000are recognised, with an open top for "over"-style bands.
- the structured
- Annual amounts must agree within 2 % or EUR 50 (whichever is larger). A concrete amount checked against a band passes when the amount sits inside the band.
- Any value that cannot be parsed produces Needs review for that document; if there is not enough parseable data to compare at all, the check is also Needs review rather than a silent pass.
The details attached to the result show the parsed annual amounts and bands, so an officer can see exactly what the engine compared.
Note
Extracted MoneyAmount values are normalized to EUR before they reach the
comparison. The comparison itself does not perform foreign-exchange
conversion, so manually imported values must already be EUR. The currency
captured on SalaryBand (and any currency symbol found in a free-text
amount) is still not converted and those values are compared as plain
decimal amounts.
Choosing a match type¶
| If the question is… | Use |
|---|---|
| "Do these identifiers match exactly?" | Exact |
| "Do these names or wordings mean the same thing?" | Fuzzy |
| "Does this period fully cover the stay?" | Range (period covers) |
| "Do these two signatures look like the same person?" | Signature |
| "Is this record valid in an external system?" | External system |
When in doubt, prefer the strictest match type that will not generate false positives on the routine variance you expect to see. Reserve Fuzzy for values a human would still call "the same" despite spelling or ordering differences.
For where a rule belongs in the overall validation model (field type, Step condition, per-document rule, or cross-document check), see Where should a new rule live?.