Scoring — Combiner

mercury.match.evaluate(link: BankLink, company: MercuryCompany) -> LinkResult

Weighted sum

Weights:

FieldWeightConstant
Name2.5NAME_WEIGHT
Email1.5EMAIL_WEIGHT
Phone1.5PHONE_WEIGHT

Threshold:

ConstantValueComparison
MATCH_THRESHOLD2.5total >= threshold → Match

Invariants

Signal shapeTotalVerdictExample
Three strong agreements5.0MatchLink 1
Full name + phone4.0MatchLinks 3, 7
Full name + email4.0MatchLink 5
Full name alone2.5MatchLink 6 (nickname-resolved)
Partial name + phone2.75MatchLink 8
Phone alone1.5MismatchLink 2
Email alone1.5Mismatch
Partial name alone1.25MismatchLink 9
Nothing agrees0.0MismatchLinks 4, 9

Rationale for the weights

The rebalanced weights reflect a specific judgment: a full first+last name match is two tokens of bio evidence on a highly specific signal, so it clears the threshold by itself (exactly at 2.5). Every narrower signal — phone alone, email alone, first-name-only, surname-only — needs a second source of agreement.

This is a simplified expression of the Fellegi–Sunter intuition: signals with high discriminating power (log m/u ratio) should dominate the decision statistic. See matching-approach for the F&S comparison.

LinkResult

@dataclass(frozen=True)
class LinkResult:
    linkId: int
    verdict: Verdict       # Match | Mismatch
    name_score: float      # [0, 1]
    email_score: float     # [0, 1]
    phone_score: float     # [0, 1]
    total: float           # weighted sum, [0, 5.5]

The CLI prints only linkId and verdict. The component scores are kept on the result so a future caller can emit graded output or audit per-field contributions without rescoring.