Entity resolution scoring (C2 slice B1) — decide whether an incoming entity mention resolves to an existing bank entity or creates a new one.
Faithful port of Hindsight's engine/entity_resolver.py _resolve_from_candidates
three-signal scorer. For each candidate the score in [0.0, 1.0] is:
name_similarity × 0.5 (how alike the surface strings are)- cooccurrence × 0.3 (share of the mention's nearby entities that this
candidate has historically co-occurred with) - temporal × 0.2 (recency: 1.0 when last seen today, linearly to 0
at a 7-day window, 0 beyond)
The best-scoring candidate is merged into iff its score is strictly greater than the threshold (default 0.6, matching Hindsight); otherwise a new entity is created. Because name-similarity alone caps at 0.5, a merge always requires at least one corroborating signal (co-occurrence or recency) — the same property that keeps Hindsight from collapsing two unrelated entities that merely share a substring.
Name-similarity is computed by Foresight.Text.SequenceMatcher.ratio/2, a
faithful port of Hindsight's difflib.SequenceMatcher(None, a, b).ratio()
(entity_resolver.py:664) on lowercased inputs — so the 0.6 threshold is a
byte-faithful constant, not a calibratable knob. Concretely, "google inc" vs
"Google" scores 0.75 → 0.375, which even with same-day recency (0.575) stays
below threshold and does NOT merge, exactly as Hindsight decides. Slice B2 adds a
semantic (embedding) term for cases lexical similarity alone cannot disambiguate
(e.g. "NYC" ↔ "New York City").
Pure and side-effect-free: candidate loading, co-occurrence assembly, and the
upsert live in the storage layer, which calls resolve/3 per mention.
Summary
Types
A resolution candidate: an existing bank entity with the signals the scorer needs.
Functions
Whether an existing entity named canonical is even eligible to be scored as a
candidate for the mention name.
Resolve name against candidates, returning {:merge, id} or :create.
Score one candidate for the mention name (with its nearby set and
event_date). Exposed for testing and diagnostics.
Types
@type candidate() :: %{ :id => term(), :name => String.t(), optional(:last_seen) => DateTime.t() | nil, optional(:cooccurring) => MapSet.t(String.t()) | [String.t()], optional(:semantic_similarity) => number() | nil }
A resolution candidate: an existing bank entity with the signals the scorer needs.
:id— opaque identifier returned on a merge:name— the candidate's canonical name:last_seen— when the entity was last mentioned (nil→ no temporal signal):cooccurring— lowercased names this entity has historically co-occurred with:semantic_similarity— B2 exceed: cosine between the mention's name embedding and this candidate's (0.0/absent → lexical-only, i.e. parity)
Functions
Whether an existing entity named canonical is even eligible to be scored as a
candidate for the mention name.
Faithful port of Hindsight's full-strategy filter
(entity_resolver.py:_resolve_entities_batch_full): case-insensitive, a match iff
the names are equal, or one is a substring of the other. The 3-signal scorer only
ever sees entities that pass this filter, so a near-miss with no substring relation
(e.g. "goggle"/"google") is never a merge candidate. Empty names are guarded out —
they are dropped upstream (fact_entities), and Python's "" in s == true would
otherwise make an empty mention match every entity.
Resolve name against candidates, returning {:merge, id} or :create.
Opts:
:nearby— other entity names mentioned alongsidename(for co-occurrence); the mention's own name is filtered out. Default[].:event_date— when this mention occurred, for the temporal signal. Defaultnil.:threshold— minimum winning score to merge (strict>). Default0.6.:labels— label taxonomy values (enum groups);:label_prefixes— label key prefixes (text groups). Whennameis a label entity, the fuzzy scorer is skipped and only an EXACT case-insensitive name match merges — faithful to Hindsight's label guard (entity_resolver.py, is_label_entity), so distinct label values that look alike are never merged. (Map-group label recursion is not ported — Foresight has no map-label config.)
Score one candidate for the mention name (with its nearby set and
event_date). Exposed for testing and diagnostics.