# How the check works

## Request flow

The minimum request supplies a `last_name`. Everything else is optional - first name, middle name, date of birth, address, up to four phone numbers, and up to four email addresses. Whichever fields you supply are evaluated against each candidate; the response tells you which ones matched.

The high-level flow is:

1. Filter the universe on `last_name`; supplied identifiers (DOB, address, phone, email) seed additional indexed lookups so candidates that match on a strong identifier are still considered even if their last-name spelling differs from the supplied one.
2. Score every candidate that comes back against the supplied fields.
3. Return either the single best candidate, or every candidate ordered best-first if `return_multiple_candidates: true` was supplied.

## Response shape

`match_results` is **always an array**. By default it contains the single best candidate (length 0 if no candidate was found, length 1 otherwise). Set `return_multiple_candidates: true` to receive multiple candidates ordered best-first.

When `return_multiple_candidates: true` is set, the response is capped at the top **5 candidates by score**. If your search matches more than 5 records, only the strongest 5 are returned; the weaker tail is dropped. Narrow your request (add a DOB, a phone, or an address) to lift the strongest candidates above the cap.

Two universe records that produce **identical per-field outcomes** against your request collapse into a single row, so the count of rows in `match_results` reflects the number of distinct outcome maps, not the count of underlying records considered. See the [Multi-candidate review](#multi-candidate-review) use case for details.

Each candidate row only includes the fields that were actually evaluated. Fields the caller did not supply are **omitted from the row entirely** (they are not returned as `null` or any placeholder value). The address block is treated as a single unit: when no address was supplied, the `address` rollup and the four `street_address` / `suburb` / `state` / `postcode` components are all omitted together.

## Match outcomes

### First name and middle name

For **first_name** and **middle_name** the response uses a graded ladder:

| Label | Meaning |
|:------|:--------|
| `match` | Exact equality after normalisation (case- and diacritic-insensitive). |
| `alias_match` | The two names share a known nickname / alias group (Bob/Robert, Jenny/Jennifer, Tony/Antonio). |
| `partial_match` | One side is exactly a single-letter initial that matches the leading letter of the other side (for example `P` vs `Peter`). Modelled on universe records that only have a first-letter initial captured for the first name. Longer-form abbreviations like `Pete` vs `Peter` are not partial matches - they are handled by the `alias_match` ladder instead. |
| `fuzzy_match` | The two names share a phonetic (metaphone) code and the same leading letter. |
| `no_match` | None of the above. |
| `no_record` | (Middle name only.) The caller supplied a middle name but the candidate has none on file. |
| `not_used` | The caller supplied this field but it was dropped because it failed validation and `ignore_errors=true`. |

Which match types are accepted is controlled per field via `first_name_matching`, `middle_name_matching`, and `last_name_matching` (each defaults to `["exact"]`).

### Last name

`last_name` uses a smaller subset of the ladder:

| Label | Meaning |
|:------|:--------|
| `match` | Exact equality after normalisation. |
| `fuzzy_match` | The two last names share a metaphone code and the same leading letter. Only returned when `last_name_matching: ["exact","fuzzy"]` is set. |
| `no_match` | Neither of the above. |
| `not_used` | Dropped because it failed validation and `ignore_errors=true`. |

### Date of birth

`dob` has its own ladder of partial outcomes for cases where the supplied date is close but not identical to the candidate's:

| Label | Meaning |
|:------|:--------|
| `match` | Exact match on year, month, and day. |
| `match_day_month_reversal` | Year matches; the supplied day and month are the same as the candidate's month and day swapped (e.g. supplied `1989-12-08` vs candidate `1989-08-12`). |
| `match_year` | Only the year matches; day and month differ and are not a reversal. |
| `no_record` | The candidate has no DOB on file. |
| `no_match` | Year does not match. |
| `not_used` | Dropped because it failed validation and `ignore_errors=true`. |

### Address

`address` is a rollup summary derived from the four supplied address components (`street_address`, `suburb`, `state`, `postcode`). Each component returns `match` / `no_match` independently and they are returned alongside the rollup so you can see exactly which parts contributed.

| Label | Meaning |
|:------|:--------|
| `match` | All four supplied address components match the candidate. |
| `match_street` | Same street number on the same street name (street-type variations like `ST` vs `RD` are allowed) but at least one of suburb / state / postcode does not match. |
| `match_locality` | The street did not match, but state matches (when supplied) and at least one of suburb or postcode also matches. |
| `no_match` | None of the above. |
| `not_used` | Dropped because it failed validation and `ignore_errors=true`. |

Whichever address input form was used (`gnaf_id`, `full_address`, or the four address parts), the address is resolved to its canonical components before evaluation, so the same rollup applies regardless of how it was supplied.

A person commonly has more than one address on file (a current address plus past addresses), but `match_results` only ever returns **one row per person**. Internally, every address on file is scored against the supplied address and the **best-matching** address is the one whose outcome appears in the response. So if a person has a current address that is an exact `match` and an older address that is only a `match_locality`, you will see `address: match` in the response - never both. Ties on rollup outcome are broken by preferring the most recently active address (largest `date_end`).

### Phone and email

Phone and email are evaluated **per slot**. Each supplied slot (`phone`, `phone2`, `phone3`, `phone4`, and the same four `email` slots) returns its own outcome in the response. Slots that were not supplied are omitted.

| Label | Meaning |
|:------|:--------|
| `match` | The supplied value for this slot matches at least one of the candidate's phones / emails on file. |
| `no_match` | The supplied value for this slot does not match any of the candidate's phones / emails on file. |
| `not_used` | Dropped because it failed validation and `ignore_errors=true`. |

So if a request supplies `phone` and `phone3`, the response carries two independent outcomes (`phone` and `phone3`); `phone2` and `phone4` are omitted. Each slot is evaluated against the candidate's full phones (or emails) list, so the slot order in the request does not matter for matching.

## Best-candidate selection

Candidates are ranked in three steps, applied in order.

1. **Strong-discriminator hits rank first.** A candidate that produces an exact match against any of `phone`, `email`, full `dob`, or the full `address` (the rollup at `match`) outranks any candidate that does not, regardless of how well the other candidate scores on names alone. `dob: match_day_month_reversal` also counts as a strong-discriminator hit, **but only when the candidate also has `first_name: match` (or `alias_match`) AND `last_name: match`** - without name corroboration, a reversed DOB is treated as a numeric coincidence and is not enough to elevate the candidate into the strong-discriminator group.
2. **Exact last-name wins within the strong-discriminator group.** Among candidates that tied on step 1, candidates with `last_name: match` rank above candidates with `last_name: fuzzy_match` - a fuzzy last-name candidate never displaces an exact last-name candidate for the top slot.
3. **Weighted score breaks remaining ties.** Per-field weights are summed and the highest sum wins.

| Field | match | partial outcomes |
|:------|------:|:-----------------|
| `first_name` | 20 | `alias_match` 16, `partial_match` 10, `fuzzy_match` 6 |
| `middle_name` | 1 | `alias_match` 1, `partial_match` 1 |
| `last_name` | 12 | `fuzzy_match` 8 |
| `dob` | 10 | `match_day_month_reversal` 7, `match_year` 4 |
| `address` | 8 | `match_street` 5, `match_locality` 3 |
| `phone` | 4 | - |
| `email` | 2 | - |

This calibration deliberately lets multiple identifiers outrank a stronger single name match. For example, within the strong-discriminator group, a candidate with `first_name: alias_match` (16) + `dob: match` (10) + `phone: match` (4) = 30 will beat a candidate with `first_name: match` (20) + `phone: match` (4) = 24.
