// East Quabbin Health — Identity & Record Matching
// Explains the identity model (EQH resident.id ↔ FHIR Patient.id ↔ Epic MRN)
// and gives coordinators a dedup / merge queue + match-rules config.
// ─── Mock data ───────────────────────────────────────────────────────
// Pending duplicate review queue (probable matches inside EQH itself)
const ID_DEDUP_QUEUE = [
{
id: 'dup-001',
confidence: 94, status: 'pending',
reasons: ['Same DOB', 'Same last name', 'Same phone', 'Address 0.8mi apart'],
foundAt: '2026-04-23T09:12:00',
trigger: 'New intake at Petersham library (Sarah K.)',
a: { id: 'r001', first: 'Margaret', last: 'Whitcomb', dob: '04/12/1953',
phone: '(978) 555-0142', address: '47 Brook Rd, Petersham, MA 01366',
created: '2025-11-14', fhirId: 'epic-Patient-a8f291', mrn: 'UMM-2841097',
source: 'CHW home visit' },
b: { id: 'r-new', first: 'Maggie', last: 'Whitcomb', dob: '04/12/1953',
phone: '(978) 555-0142', address: '47 Brook Road, Petersham MA',
created: '2026-04-23', fhirId: null, mrn: null,
source: 'Library signup kiosk' },
},
{
id: 'dup-002',
confidence: 78, status: 'pending',
reasons: ['Same DOB', 'Same first name', 'Same town', 'Different last name (née?)'],
foundAt: '2026-04-22T14:33:00',
trigger: 'Cohort analyst (Marcus D.)',
a: { id: 'r003', first: 'Rosa', last: 'Mendoza', dob: '08/22/1971',
phone: '(978) 555-0203', address: '88 Main St Apt 2, Barre, MA 01005',
created: '2024-06-02', fhirId: 'epic-Patient-c4d812', mrn: 'UMM-5829104',
source: 'Health fair' },
b: { id: 'r-new-2', first: 'Rosa', last: 'Mendoza-Ruiz', dob: '08/22/1971',
phone: '(413) 555-8821', address: '88 Main Street #2, Barre',
created: '2026-03-18', fhirId: 'epic-Patient-f22a09', mrn: 'UMM-6104772',
source: 'Self-serve online' },
},
{
id: 'dup-003',
confidence: 62, status: 'pending',
reasons: ['Same last name', 'Same town', 'DOB 1 year apart', 'Phone differs'],
foundAt: '2026-04-21T11:05:00',
trigger: 'Automated nightly scan',
a: { id: 'r017', first: 'Stanley', last: 'Beauchamp', dob: '03/02/1937',
phone: '(978) 555-1683', address: 'Main St, Hubbardston',
created: '2024-09-11', fhirId: 'epic-Patient-11bb40', mrn: 'UMM-9382016',
source: 'CHW home visit' },
b: { id: 'r-new-3', first: 'Stan', last: 'Beauchamp', dob: '03/02/1938',
phone: '(978) 555-4419', address: '22 Oak Ln, Hubbardston MA',
created: '2026-02-08', fhirId: null, mrn: null,
source: 'Senior center sign-up' },
},
];
const ID_MATCH_RULES = [
{ field: 'Date of birth', weight: 30, type: 'exact', current: true },
{ field: 'Last name', weight: 25, type: 'fuzzy (Jaro-Winkler ≥ 0.85)', current: true },
{ field: 'First name', weight: 15, type: 'fuzzy + nickname table (Maggie↔Margaret)', current: true },
{ field: 'Phone number', weight: 20, type: 'normalized digits', current: true },
{ field: 'Address (street)', weight: 10, type: 'normalized + geocode within 1mi', current: true },
{ field: 'Email', weight: 10, type: 'exact (lowercased)', current: true },
{ field: 'SSN last 4', weight: 40, type: 'exact', current: false, note: 'Not collected by EQH' },
];
const ID_STATS = {
residents: 247,
epicLinked: 189,
epicLinkRate: 76,
pendingReview: 3,
mergedThisMonth: 7,
autoMatchedThisMonth: 34,
ambiguous: 2,
};
// ─── Component ───────────────────────────────────────────────────────
const ChwIdentityPage = () => {
const [selected, setSelected] = React.useState(ID_DEDUP_QUEUE[0].id);
const [mergePrimary, setMergePrimary] = React.useState('a');
const [showMerge, setShowMerge] = React.useState(false);
const [resolved, setResolved] = React.useState({}); // duplicate id -> 'merged' | 'distinct'
const dup = ID_DEDUP_QUEUE.find(d => d.id === selected);
return (
Identity & record matching
How East Quabbin resident records line up with FHIR Patient.id in Epic — and how we catch duplicates when someone signs up twice.
{/* Stats strip */}
{/* Identity model explainer */}
{/* Dedup queue */}
Dedup review queue
Probable duplicates flagged by our matcher. High-confidence merges (≥95) run automatically; anything below lands here.
{ID_DEDUP_QUEUE.filter(d => !resolved[d.id]).length} pending
{ID_DEDUP_QUEUE.map(d => {
const r = resolved[d.id];
return (
{ setSelected(d.id); setShowMerge(false); }}
style={{
padding: '14px 16px',
borderBottom: '1px solid var(--eq-line)',
background: selected === d.id ? 'var(--eq-surface-2)' : 'transparent',
cursor: 'pointer',
borderLeft: selected === d.id ? '3px solid var(--eq-sage-deep)' : '3px solid transparent',
}}>
{d.a.first} {d.a.last}
·
DOB {d.a.dob}
{d.confidence}%
{r === 'merged' ? '✓ Merged' : r === 'distinct' ? '◦ Kept separate' : d.reasons[0]}
);
})}
{dup && (
{resolved[dup.id] ? (
setResolved(r => { const n = {...r}; delete n[dup.id]; return n; })} />
) : showMerge ? (
setShowMerge(false)}
onComplete={() => { setResolved(r => ({ ...r, [dup.id]: 'merged' })); setShowMerge(false); }} />
) : (
setShowMerge(true)}
onDistinct={() => setResolved(r => ({ ...r, [dup.id]: 'distinct' }))} />
)}
)}
{/* Match rules config */}
{/* Future-signup scenario */}
);
};
// ─── Identity model explainer ─────────────────────────────────────────
const IdModelCard = () => (
How we identify a resident
Every resident has one canonical EQH id. External systems (Epic, MassHealth, etc.) each contribute their own identifier as an alias . We never overwrite the canonical record — we add identifiers to it.
Stored shape on a linked resident
{'{'}
id : "r001" ,
firstName : "Margaret" , lastName : "Whitcomb" ,
identifiers : [
{'{ system: "urn:epic:UMMemorial", value: "UMM-2841097", use: "official" },'}
{'{ system: "urn:ietf:rfc:3986", value: "epic-Patient-a8f291", use: "fhir" },'}
{'{ system: "urn:masshealth", value: "MH-44718023", use: "secondary" }'}
],
matchConfidence : 98, matchedAt : "2025-11-14" ,
mergedFrom : []
{'}'}
);
const IdCol = ({ color, bg, label, id, sample, kind, rules }) => (
{label}
{id}
e.g. {sample}
{kind}
{rules.map((r, i) => {r} )}
);
// ─── Stat cell ─────────────────────────────────────────────────────
const IdStat = ({ label, value, sub, tone }) => {
const tones = {
epic: { color: '#C8102E', bg: 'color-mix(in srgb, #C8102E 8%, var(--eq-surface))' },
warn: { color: 'var(--eq-warn)', bg: 'color-mix(in srgb, var(--eq-warn) 10%, var(--eq-surface))' },
ok: { color: 'var(--eq-sage-deep)', bg: 'color-mix(in srgb, var(--eq-sage) 14%, var(--eq-surface))' },
};
const s = tones[tone] || { color: 'var(--eq-ink)', bg: 'var(--eq-surface)' };
return (
{label}
{value}
{sub &&
{sub}
}
);
};
// ─── Confidence meter ────────────────────────────────────────────
const ConfidenceBar = ({ value }) => (
);
const confColor = (v) => v >= 90 ? 'var(--eq-sage-deep)' : v >= 75 ? '#b8860b' : 'var(--eq-warn)';
// ─── Side-by-side compare ─────────────────────────────────────────
const IdCompare = ({ dup, onMerge, onDistinct }) => {
const fields = [
{ k: 'first', l: 'First name' },
{ k: 'last', l: 'Last name' },
{ k: 'dob', l: 'Date of birth' },
{ k: 'phone', l: 'Phone' },
{ k: 'address', l: 'Address' },
{ k: 'source', l: 'Source' },
{ k: 'created', l: 'Created' },
{ k: 'fhirId', l: 'FHIR Patient.id', mono: true },
{ k: 'mrn', l: 'Epic MRN', mono: true },
];
return (
<>
Why flagged
{dup.reasons.map((r, i) => (
{r}
))}
Flagged {formatRelTime(dup.foundAt)} · {dup.trigger}
Existing · {dup.a.id}
New signup · {dup.b.id}
{fields.map((f, i) => {
const av = dup.a[f.k], bv = dup.b[f.k];
const same = av === bv;
return (
0 ? '1px solid var(--eq-line)' : 'none', fontSize: 12, fontWeight: 600, color: 'var(--eq-ink-2)' }}>{f.l}
0 ? '1px solid var(--eq-line)' : 'none', fontSize: 13, fontFamily: f.mono ? 'var(--eq-mono, monospace)' : 'inherit' }}>{av || — }
0 ? '1px solid var(--eq-line)' : 'none', fontSize: 13, fontFamily: f.mono ? 'var(--eq-mono, monospace)' : 'inherit',
background: same ? 'transparent' : 'color-mix(in srgb, var(--eq-warn) 9%, transparent)',
fontWeight: same ? 400 : 600 }}>
{bv || — }
);
})}
Merge records…
Mark as distinct people
Ask CHW to verify
>
);
};
// ─── Merge wizard ─────────────────────────────────────────────────
const IdMergeWizard = ({ dup, primary, setPrimary, onCancel, onComplete }) => {
const p = dup[primary];
const s = dup[primary === 'a' ? 'b' : 'a'];
const [picks, setPicks] = React.useState({});
const [note, setNote] = React.useState('');
const conflictFields = ['first', 'last', 'phone', 'address'].filter(k => dup.a[k] !== dup.b[k]);
return (
<>
Merge records
One resident survives with all identifiers and history. The other id is archived with a redirect.
1. Primary record (survives)
{['a', 'b'].map(k => {
const r = dup[k];
const sel = primary === k;
return (
setPrimary(k)} style={{
padding: 14, textAlign: 'left',
background: sel ? 'color-mix(in srgb, var(--eq-sage) 12%, var(--eq-surface))' : 'var(--eq-surface)',
border: '1.5px solid ' + (sel ? 'var(--eq-sage-deep)' : 'var(--eq-line)'),
borderRadius: 'var(--eq-r-md)', cursor: 'pointer', transition: 'all 140ms',
}}>
{r.first} {r.last}
{r.id}
Created {r.created} · {r.source}
{r.mrn && MRN {r.mrn}
}
);
})}
{conflictFields.length > 0 && (
2. Resolve conflicts
{conflictFields.map((k, i) => {
const chosen = picks[k] || primary;
return (
0 ? '1px solid var(--eq-line)' : 'none' }}>
{k === 'first' ? 'First name' : k === 'last' ? 'Last name' : k === 'phone' ? 'Phone' : 'Address'}
{['a', 'b'].map(side => (
setPicks(o => ({ ...o, [k]: side }))}
style={{
padding: '10px 14px', textAlign: 'left',
background: chosen === side ? 'color-mix(in srgb, var(--eq-sage) 12%, transparent)' : 'var(--eq-surface)',
border: 'none',
borderLeft: '1px solid var(--eq-line)',
fontSize: 13, cursor: 'pointer',
fontWeight: chosen === side ? 600 : 400,
color: chosen === side ? 'var(--eq-sage-deep)' : 'var(--eq-ink)',
}}>
{chosen === side && ✓ }
{dup[side][k]}
))}
);
})}
Both records\u2019 identifiers (FHIR Patient.id, MRN, MassHealth) are kept on the survivor.
)}
{conflictFields.length > 0 ? '3' : '2'}. Merge note (for audit log)
What happens on merge
{s.id} → redirect to {p.id}, marked merged
Both identifiers (MRN, FHIR Patient.id) attached to survivor
Activity, consents, and SDoH answers combined — newest wins on conflict
Merge logged in audit trail under your name
Reversible for 30 days
Complete merge
Back
>
);
};
// ─── Resolved view ────────────────────────────────────────────────
const IdResolvedView = ({ action, dup, onUndo }) => (
{action === 'merged' ? '✓ Records merged' : '◦ Kept as distinct people'}
{action === 'merged'
? `${dup.b.id} now redirects to ${dup.a.id}. Both MRNs are attached to the surviving record.`
: `These are different people. They\u2019ll stay separate and the matcher won\u2019t flag them again.`}
Undo
);
// ─── Match rules config card ─────────────────────────────────────
const IdMatchRulesCard = () => {
const total = ID_MATCH_RULES.filter(r => r.current).reduce((a, r) => a + r.weight, 0);
return (
Match rules
Each field contributes to a probabilistic match score. Tuned with the Fellegi-Sunter model, calibrated against ground truth from our first 200 enrollments.
Edit weights…
Field
Weight
Match type
Status
{ID_MATCH_RULES.map((r, i) => (
{r.field}
{r.current ? r.weight : '—'}
{r.type}
{r.current ? (
Active
) : (
{r.note || 'Disabled'}
)}
))}
Active weight total: {total} · normalized to 0–100 score before threshold comparison.
);
};
const Threshold = ({ label, color, from, to }) => (
);
// ─── Repeat-signup explainer ─────────────────────────────────────
const IdRepeatSignupCard = () => (
When someone signs up again
Every new intake runs through the matcher before a record is created. The coordinator sees both EQH and Epic matches inline — no duplicates unless they are confirmed as distinct people.
Scenario A · Same person, different wording
Margaret signed up in Nov; "Maggie Whitcomb" signs up today at the library. Matcher hits 94%, lands in review queue. Coordinator merges — one record, both touchpoints attached.
Scenario B · Truly new person
Score stays below 70 — no match prompt shown, new resident.id minted, Epic queried for a fresh FHIR Patient.id on first clinical read.
);
const FlowStep = ({ n, title, body }) => (
);
const FlowArrow = () => (
→
);
// ─── utils ───────────────────────────────────────────────────────
function formatRelTime(iso) {
const d = new Date(iso);
const diff = Date.now() - d.getTime();
const h = Math.floor(diff / 3600000);
if (h < 1) return 'just now';
if (h < 24) return h + 'h ago';
const days = Math.floor(h / 24);
return days + 'd ago';
}
window.ChwIdentityPage = ChwIdentityPage;