---
title: "Deduplicate postal addresses in C#"
url: https://recursiveai.org/onecontact/cookbook/deduplicate-postal-addresses/
description: "Group postal address variants by versioned keys in C# with OneContact. Keep unit numbers separate and preserve canonical display text."
version: "1.0.0"
---

# Deduplicate postal addresses in C#

**OneContact** is a free, open-source contact data library for .NET from Recursive AI. MIT or Apache-2.0, no native dependencies, no network access.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneContact) · [GitHub](https://github.com/recursive-ai-oss/onecontact-csharp) · [Docs](https://recursiveai.org/onecontact/docs/) · [Download example (.zip)](https://recursiveai.org/onecontact/cookbook/deduplicate-postal-addresses/deduplicate-postal-addresses.zip)

An address key is a normalized comparison string built from an address’s parts. Pass the country (`Region`) you know when parsing; compare keys to group spelling and layout variants.

```shell
dotnet add package RecursiveAI.OneContact
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

```csharp
using RecursiveAI.OneContact;

var first = PostalAddress.Parse(
    "10-123 Main St, Toronto ON M5V 2T6",
    Region.CA);
var second = PostalAddress.Parse(
    "123 Main Street Apt 10, Toronto ON M5V 2T6",
    Region.CA);
Console.WriteLine(first.Key == second.Key);
```

For a list, use `GroupBy(address => address.Key)` as shown in the [full program](#full-program).

```text
True
```

Unzip [the example](https://recursiveai.org/onecontact/cookbook/deduplicate-postal-addresses/deduplicate-postal-addresses.zip), then `dotnet run`.

## Full program

These made-up US addresses include a ZIP+4 variant and a different apartment. The first address in each group supplies its display text.

```csharp
using RecursiveAI.OneContact;

string[] inputs = [
    "123 North Main Street Apartment 4, Springfield IL 62704-1234",
    "123 N Main St Apt 4, Springfield IL 62704",
    "123 N Main St Apt 5, Springfield IL 62704"
];
var addresses = inputs.Select(text => PostalAddress.Parse(text, Region.US));
foreach (var group in addresses.GroupBy(address => address.Key))
{
    Console.WriteLine($"{group.Count()} candidate(s): {group.Key}");
    Console.WriteLine(group.First().Canonical);
}
```

```text
2 candidate(s): us|123 north main street unit 4|springfield|il|62704
123 N MAIN ST APT 4, SPRINGFIELD IL 62704-1234
1 candidate(s): us|123 north main street unit 5|springfield|il|62704
123 N MAIN ST APT 5, SPRINGFIELD IL 62704
```

If parsing fails, `PostalAddress.Parse` throws `ContactParseException`; the sample stops rather than dropping that address.

## Variations

### Pin the comparison rule

```csharp
using RecursiveAI.OneContact;

var address = PostalAddress.Parse(
    "123 Main St, Springfield IL 62704", Region.US);
Console.WriteLine(address.GetKey(KeyVersion.V1));
```

```text
us|123 main street|springfield|il|62704
```

Store the key version alongside a persisted key. A published version’s output never changes; future rule changes use a new version.

## Related recipes

- [Parse a postal address](https://recursiveai.org/onecontact/cookbook/parse-postal-address/)
- [Standardize a Canadian address](https://recursiveai.org/onecontact/cookbook/standardize-canadian-address/)
- [Deduplicate contacts](https://recursiveai.org/onecontact/cookbook/deduplicate-contacts/)

## The short answer

Parse each address with the country context you actually know, then group by `PostalAddress.Key`. Keep the canonical address as display text, not the key. Equal keys are duplicate candidates for your application to review; they do not establish a person’s identity or prove that mail can be delivered.

The snippet at the top compares two Canadian unit layouts. The full program groups US street abbreviations and a ZIP+4 variant while keeping apartment 5 separate from apartment 4. Neither example performs fuzzy matching or removes a record automatically.

## How do I remove duplicate postal addresses from a C# list?

Compare parsed delivery components rather than the original strings. A string comparison would see “North” and “N” as different text. The address key expands the directional and street type into common forms and normalizes the unit designator. LINQ’s `GroupBy` then collects the records that share those components, as the full program shows.

Do not lowercase and strip punctuation from an entire address as a substitute for parsing. A house-number range and a unit separator can affect what an address means. OneContact builds the key from fields, retaining delivery-line order and unit numbers. It folds case, Latin diacritics and word punctuation using the rules described in [the address key documentation](https://recursiveai.org/onecontact/docs/address-keys/).

For an import, retain an input record ID and original text beside the parsed value. Review failed parses separately. Keeping the first canonical address is the sample’s display policy, not a library rule about which source is correct. A business application might instead keep the most recently confirmed address or ask the user to choose.

## How do I remove duplicates by an address property in C#?

Use the address key as the grouping property, not the canonical string. The canonical form is for reading; the key is for comparison. Store both if the database needs both roles. A key contains the country, delivery lines, locality, administrative area and postal code, joined by separators.

The [version 1 rules](https://recursiveai.org/onecontact/docs/address-keys/) deliberately use only the five-digit part of a US ZIP+4. Recipient, organization and attention lines never enter the key. Consequently, two people at one delivery address can share a key, and an address with extra ZIP digits can match one without them. That is appropriate for a delivery-address candidate group, not for merging customer accounts.

Numbered unit designators such as “apt” and “suite” normalize to “unit”, but their numbers remain. Canadian `10-123 Main St` and `123 Main Street Apt 10` match; a different unit does not. Floors and buildings retain their own dictionary words rather than all becoming numbered units.

When persisting comparison keys, explicitly use `GetKey(KeyVersion.V1)` and save that version with the value. The versioned output is stable; a new comparison policy should not silently reinterpret old stored keys. Preserve the underlying address so you can compute another version later without treating the old comparison key as recoverable address text.
