Recursive AI

Diagnose an undetermined address country 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.

An address parser can recover fields without having enough evidence to choose a country.

Terminal
dotnet add package RecursiveAI.OneContact

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

C#
using RecursiveAI.OneContact;
 
string text =
    "Unter den Linden 77, 10117 Berlin";
var status = PostalAddress.TryParse(
    text.AsSpan(), out var partial, out _);
Console.WriteLine(status);
Console.WriteLine(partial.Country);
Console.WriteLine(partial.Flags);
var address = PostalAddress.Parse(
    text, Region.Parse("DE"));
Console.WriteLine(address);
Text
Done
ZZ
CountryUndetermined
Unter den Linden 77, 10117 BERLIN

Postcodes with the five-digit shape of 10117 occur in several countries, so this text alone does not establish the country.

Unzip the example, then dotnet run.

Full program

This Berlin address has a postcode shape shared by multiple countries. The retry uses Germany because the caller explicitly knows that country.

C#
using RecursiveAI.OneContact;
 
string text = "Unter den Linden 77, 10117 Berlin";
var status = PostalAddress.TryParse(text.AsSpan(), out var partial, out var detail);
Console.WriteLine($"{status}: {partial.Country}, {partial.Flags}");
Console.WriteLine($"Locality: {partial.Locality}; postcode: {partial.PostalCode}");
foreach (var issue in detail.Issues)
    Console.WriteLine(issue.Reason.Id);
Console.WriteLine($"Happy path: {PostalAddress.TryParse(text, out _)}");
var address = PostalAddress.Parse(text, Region.Parse("DE"));
Console.WriteLine($"{address.Country}: {address.Canonical}");
Text
Done: ZZ, CountryUndetermined
Locality: Berlin; postcode: 10117
address.country-undetermined
Happy path: False
DE: Unter den Linden 77, 10117 BERLIN

The simple Boolean TryParse overload (the happy path) returns false for an undetermined country; Parse throws ContactParseException with address.country-undetermined.

Variations

Catch the stable reason from Parse

C#
using RecursiveAI.OneContact;
 
try
{
    PostalAddress.Parse("Unter den Linden 77, 10117 Berlin");
}
catch (ContactParseException error)
{
    Console.WriteLine(error.Reason.Id);
}
Text
address.country-undetermined

Inspect disputed fields

C#
using RecursiveAI.OneContact;
 
var status = PostalAddress.TryParse(
    "Muster 12-14, 10115 Berlin".AsSpan(), out var partial, out var detail);
Console.WriteLine($"{status}: {partial.Flags}");
foreach (var issue in detail.Issues)
    Console.WriteLine(issue.Reason.Id);
Text
Done: CountryUndetermined, FieldsDisputed
address.country-undetermined
address.fields-disputed

Here the country candidates disagree about the house/unit interpretation of 12-14. The partial result leaves disputed elements out instead of choosing one.

The short answer

Use the status-returning overload PostalAddress.TryParse(span, out value, out detail) when you need a partial address reading. Check both the status and the returned flags. Done can coexist with CountryUndetermined; it does not mean the country has been established. Retry with an explicit Region only when you have that context independently.

Use the simpler Boolean TryParse overload when your application needs a usable country-specific address or a refusal. It rejects the same ambiguous input rather than returning a partial value as a successful address.

Why is my address parser’s country undetermined?

A postcode is not always a country identifier. Several countries share four-digit or five-digit patterns. Without a country hint or distinguishing evidence in the text, selecting the first country that accepts a pattern would turn missing information into an unsupported assumption.

OneContact’s country-undetermined rules try the candidate country readings. When the candidates agree on the fields, the low-level result retains those fields and leaves Country as Region.Unknown, printed as ZZ. It sets AddressFacts.CountryUndetermined and carries the warning reason address.country-undetermined in the parse detail.

That is why the full program prints Done and an unknown country together. It has recovered Berlin and the postcode, but it has not established Germany. The happy-path Boolean overload returns false, so a checkout form does not accidentally accept a countryless result as a complete shipping address.

The undetermined canonical text stays as written apart from trimming and joining input parts; no country’s postal style is applied. Coverage is structural and validation reports the missing country. A partial key can begin with zz, so keep partial readings separate from accepted country-specific addresses in a production import.

How do I parse an address with an explicit country in C#?

Pass a known Region as the second argument. The retry in the full program uses Region.Parse("DE"); the hinted result prints DE and Germany’s canonical rendering. The country must come from something you know, such as a user-selected country or a trusted field in the source record, not from the ambiguous postcode itself.

For a batch import, separate records needing country confirmation from records that failed for other reasons. A hint can select the country’s parser, but it does not establish delivery or resolve every malformed address. Countries without free-text support can still refuse the input. See the overload contract for the distinction between the happy path and low-level engine.

The disputed-field variation is a further case: all candidates agree on the postal code but disagree on some address elements. The engine retains only agreed elements, sets FieldsDisputed with CountryUndetermined, and adds address.fields-disputed. Do not fill those missing elements from a candidate you arbitrarily prefer.

For logging, use issue.Reason.Id, or the caught exception’s Reason.Id, rather than parsing a human-readable message. Those IDs are the documented stable wire text. If you switch on the reason enumeration instead, retain a fallback for new members. Keep the original input alongside the partial result so a person can resolve the country and rerun parsing without reconstructing the source from incomplete fields.

All onecontact recipes