nextcloud / nextcloud/contacts

RFC: Homonym-safe duplicate detection & merge for Contacts

Open
#5,619 2 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
642
Forks
220
Avg merge
14h 39m
Merged PRs (30d)
51

Description

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:

  • #70 "Merge duplicate contacts" — the canonical request (had a bounty). Also asks for "a command to
    scan all your contacts and consolidate them"
    → addressable later as an occ command (see follow-ups).
  • #1088 "Merge and tidy-up contacts feature" — describes almost exactly this proposal: mark contacts as
    duplicate and merge, scan for possible duplicates and ask what to do, and detect the same field (ORG,
    TITLE) defined multiple times in one contact.
  • #2757 "Add the possibility to delete duplicated contacts", #286 (similar entries as duplicates).

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 #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 #764 (and is relevant to #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 #70).
  • A server-side merge endpoint, if preferred over client-side CardDAV writes.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with docs/duplicate-detection.md and docs/contribution/pr-split-plan.md to understand the proposed architecture and staged work. This is an RFC, so wait for maintainer decisions on client-side integration, homonym detection, editor scope, and CSV export before choosing an implementation entry point. Done means the agreed feature slice is implemented in focused, build- and test-gated PRs.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
frontend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
28/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.