Recursive AI
LibraryC#Available

onecontact

Phone numbers, email and web addresses, postal addresses and names for .NET, in C#. Parse, validate, format and compare them, and find them in web pages and text, with no native code and no network.

Open source, MIT or Apache-2.0, made by Recursive AI. Version 1.0.0 is on NuGet. It runs on rules and markup alone; trained models follow in 1.1.

dotnet add package RecursiveAI.OneContact

A quick look

One call per value: a number in E.164 with its type, an email in canonical form, an address in the postal authority’s style, and the people on a page.

The output shown is from a run on 1.0.0 from NuGet, on made-up inputs. Getting started has the whole program.

Getting started
Program.csC#
PhoneNumber phone = PhoneNumber.Parse(
    "(202) 555-0145", Region.US);
Console.WriteLine($"{phone.E164} {phone.Type} valid={phone.IsValid}");
// +12025550145 FixedLineOrMobile valid=True
 
EmailAddress email = EmailAddress.Parse("Jo.Example+news@Example.com");
Console.WriteLine(email.Canonical);
// Jo.Example+news@example.com
 
PostalAddress home = PostalAddress.Parse(
    "123 north main street suite 400, springfield, il 62704");
Console.WriteLine(home.ToString(AddressStyle.Postal, AddressLayout.SingleLine));
// 123 N MAIN ST STE 400, SPRINGFIELD IL 62704
 
var people = ContactExtractor.Extract(
    "<p>Jo Example, Head Baker</p><p>Phone: (217) 555-0188</p>");
Console.WriteLine(
    $"{people[0].Kind}: {people[0].Name?.Text} | {people[0].Phones[0].Text}");
// Person: Jo Example | (217) 555-0188
What it does

What it does

Phone numbers

For every region: “parse, validate, number type, every format, an as-you-type formatter, a matcher with leniency levels, short numbers, example numbers.” Against libphonenumber 9.0.43, on generated cases for every region and non-geographic code: “0 of 2,819,630 results (141,206 cases, 2 seeds)” mismatched, and 0 of 969,881 for the formatter, matcher and short numbers.

Phone numbers

Geocoder, carrier and time zones

An add-on package, RecursiveAI.OneContact.Phones.Geo. Against libphonenumber’s geocoder 3.43 and carrier 2.43, on generated cases for every calling code: “0 of 1,159,521 cases (2 seeds)” mismatched.

Geocoder and carrier

Email

“RFC 5322, RFC 6531, IDNA domains” with “provider-aware keys and role, free-provider and disposable flags.” The project’s own corpus of the RFCs’ examples passes 205 of 205; the public isemail list passes 164 of 164, “with 35 documented expectation overrides.”

Email addresses

URLs and domains

“WHATWG URL and the Public Suffix List, tracking parameters removed.” On the web-platform-tests conformance data, urltestdata.json passes 896 of 896, the setters and toascii files 278 of 278 and 87 of 87, and UTS #46’s IdnaTestV2.json 2,676 of 2,676. The list’s own test_psl.txt passes 78 of 78.

Web addresses

Postal addresses

“A structural layer for every country (fields, layout, required fields, postcode patterns)”: against libaddressinput’s released build, on generated cases for every region, “0 of 32,883, with 4 documented intended differences.” Standardization to the authority’s style reproduces 206 of 206 published examples of USPS Publication 28 and Canada Post, and 316 of 316 published by the other posts and the UPU.

The structural layer

Names and companies

“Western-pattern person names (titles, suffixes, particles, order, several people in one string, casing restoration) and company names with ISO 20275 legal forms.” In the changelog’s words, “Nothing is inferred about gender, origin or salutation.”

Person names

Matching keys

Every value has a canonical form and a versioned comparison key, and Contacts.SameKey compares two values by it. In the README’s words, “The same key means the same thing, whatever the kind.” “KeyVersion.V1 is frozen with this release: from here on, any change to a key rule ships as a new version.”

The address key

Extraction from pages and text

“People, organizations, addresses, phones, emails and websites from pages (JSON-LD, microdata, RDFa Lite, h-card, mailto:/tel:, <address>, visible text) and from plain text,” in RecursiveAI.OneContact.Extraction, on OneHtml 1.6.0. Every field keeps its source and offsets.

Extraction

Offline, NativeAOT and trimming

“Nothing in the packages opens a socket, resolves a name or downloads a file.” The core references the .NET base class library only, with no native code, and is NativeAOT and trim clean. The data tables are compiled into the assembly.

Overview
How it reads

Rules-only in 1.0

In the README’s words: “Version 1.0 runs on rules and markup alone. The trained model packages (RecursiveAI.OneContact.Models.Tier1, .Models and .Models.Accurate) follow in 1.1, after OneNLP 1.0.”

  • What the rules read. “What is certain (phone metadata, postcode patterns, region names, authority abbreviations, legal forms, titles, line structure),” and ParseDetail says so per field.
  • What they refuse. “They never read a text in the wrong country and never print a value the text does not contain. Text they cannot read is refused with a reason: when only a trained model could fill a field, the reason names the package that does so, available from 1.1.”
  • Model results. Not published for 1.0. The README: “Trained-model quality rows … are published with the model packages in 1.1.”
How the models plug in
Limits

Known gaps at 1.0.0

  • Free-text addresses by country. Full parsing, standardization, validation and expansion cover “the United States, Canada, Mexico, Australia, the United Kingdom and Crown dependencies, the Latin-script countries of the EU, Switzerland, Norway, Iceland, Liechtenstein and the small European states.” Every other country has the structural layer.
  • Text the rules can’t place. It’s refused, not guessed. When the country can’t be told, address.country-undetermined “asks you to give one.”
  • Names. Person names are “Western-pattern.”
  • Carriers. “The carrier mapper names the carrier a number range was first assigned to. Where number portability exists a number can have moved to another carrier, and no offline table can know that.”
  • Custom phone formats. libphonenumber’s formatByPattern has “no equivalent (vanity letters are converted when parsing; custom patterns are not supported).”
  • Pages are yours to supply. Extraction reads “a set of pages from one site that you supply (it never fetches).”
Every result, in the overview

Status and packages

Go further