Skip to content

RFC: Homonym-safe duplicate detection & merge for Contacts #5619

Description

@Darlio

Summary

I'd like to contribute a duplicate-detection and merge feature to Contacts — the capability tracked
by #70 (open since 2016), #1088, #2757 and #286. It's built, and used daily on a real
~11,000-contact address book running the current app (Vue 3 / @nextcloud/vue v9) on Nextcloud 33
. I'd
like to upstream it as a series of small, focused PRs — but I want your input on design and scope first.

The guiding principle throughout is safety: it must never merge two different people on its own, and
never lose contact data
. Every merge is undoable.

Problem

Address books accumulate duplicates — the same person imported twice, synced from two accounts, or entered
by two people. Contacts has no built-in way to find and merge them. This is a long-standing, much-requested
gap:

The risk with any auto-merge is fusing two different people (homonyms) or silently dropping data — so
safety has to be designed in, not bolted on.

Proposed design

A Duplicates view that sorts clusters into clearly-separated lanes by safety posture:

  • Duplicates to merge — share an exact email/phone and compatible names → safe to auto-merge.
  • Needs your choice — confident duplicate but members disagree on a single-valued field (usually the
    name) → interactive editor only, never auto-merged.
  • Entries to review — share a contact point but names are incompatible (a shared family mailbox) →
    review only, low confidence.
  • Possible homonyms (opt-in) — share a name but no email/phone → review-only, never bulk-merged.
  • Kept separate / Incomplete — housekeeping lists.

The view has group-scope and free-text filters (search across name/email/phone/address) so large books
stay navigable.

Key safety mechanisms:

  • Exact-key grouping only for auto-detection (shared email/phone); fuzzy/name-only matching is confined
    to the opt-in, review-only homonym finder — never auto-merged, never in "Merge all".
  • Data-preserving union merge: multi-valued fields unioned, single-valued gap-filled, exactly one FN
    (the CardDAV validator rejects >1). Repeated ORG/TITLE within a card are surfaced (addresses Merge and tidy-up contacts feature #1088).
  • Persist-before-delete, session undo/redo, and a source is deleted only if it can be re-created.
  • A confidence score modelled on human judgement (identical name + shared contact → near-certain; a
    contradicting birthday drops it out of the auto-merge band).
  • An interactive N-way editor for full curation: pick which name survives, keep/discard each value,
    and include/exclude which contacts are actually the same person.

Architecture and the data-safety invariants are documented in docs/duplicate-detection.md, plus a user
guide.

Status

  • Low integration cost. Implemented for main (Vue 3 / @nextcloud/vue v9); it currently applies,
    builds, npm testes and lints cleanly against the latest main — no conflicts, ~200 unit tests green,
    ESLint 0/0 on the feature files. (Naturally I'll adapt it to whatever you'd like changed.)
  • Reviewable. The pure logic (detection, merge, confidence, homonym finder, store, search) is unit-
    tested with no dependency on the Contact model, and the change is split into small dependent PRs
    (below) rather than one large drop.
  • Battle-tested, not a prototype. It's been in daily use on an ~11k-contact address book — which
    is how the real-world edge cases (and a couple of Vue-3-runtime bugs) were found and fixed.
  • I've put real effort into correctness, data-safety, accessibility, i18n and matching the project's
    idioms — but I'd genuinely welcome your review and will follow your lead on design and conventions.
  • SPDX/REUSE headers on new files; strings are single translatable units; deep imports follow the v9
    convention.

Questions for maintainers

  1. Would you accept this feature in principle? If so, as a series of focused PRs (see below)?
  2. Client-side vs server-side merge. It currently merges client-side (CardDAV writes) to stay
    self-contained and require no server changes. Would you prefer a server-side merge endpoint, or is
    client-side acceptable for a first version (with a server endpoint as a follow-up)?
  3. Scope of a first version. Is the opt-in homonym finder (name-only, review-only) welcome in the
    initial contribution, or would you rather it land as a separate later PR?
  4. UX conventions. Any preferences on the entry point (nav item vs. an action), and on the component for
    the dense merge editor (NcCheckboxRadioSwitch vs. styled native inputs with radiogroup semantics for
    the value-picker matrix)?
  5. CSV export — an independent, generic CSV export (configurable columns/delimiter/encoding) that grew
    alongside this; it addresses the long-standing request Export a list with contacts #764 (and is relevant to Export to other file formats than .vcf, e.g. XML #1621, which
    asks for export formats beyond .vcf). In scope here, or better as its own unrelated PR?

Proposed PR breakdown (details in docs/contribution/pr-split-plan.md)

  1. Detection + lanes + filters + data-preserving auto-merge + undo/redo (the foundation).
  2. The interactive N-way merge editor.
  3. The opt-in homonym finder.
  4. (Optional/independent) CSV export.

Each PR is cut as a build- and test-gated commit (every commit DCO Signed-off-by) so the tree builds
and tests green at every step. Happy to adjust the split, scope, and design to fit how you'd like this to
land.

Possible follow-ups (not in the initial PRs)

  • An occ contacts:dedup command for headless scan/merge (explicitly requested in Merge duplicate contacts #70).
  • A server-side merge endpoint, if preferred over client-side CardDAV writes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions