Recursive AI

Migrating to OneContact

All onecontact docs

Three reads-like tables for people who know the libraries OneContact replaces. Each row names a call or concept from the other library's published documentation and the OneContact call that does the same job. There is no compatibility layer and no copied API shape: OneContact uses .NET conventions (value types, Parse/TryParse, enums, OperationStatus), so the names differ and a few ideas are folded together or left out on purpose. Where a feature has no equivalent, the row says so.

The other libraries are described here only from their public documentation (their READMEs, Javadoc, wikis and user guides). If a behaviour matters to you, check it against your data: the phone and structural-address layers are tested for exact agreement with their reference (see the Results section of the README), the parsing layers are measured, not identical.

Habits that change for everyone:

  • No default region, no default culture. A national phone number needs an explicit Region; text that is not an international number fails with phone.region-required rather than guessing. Nothing reads CultureInfo.
  • Value types. PhoneNumber, EmailAddress, WebAddress, PostalAddress, PersonName and CompanyName are immutable structs. == compares the canonical form; compare keys explicitly with Contacts.SameKey or .Key.
  • Errors. Parse throws ContactParseException (a FormatException) carrying a ReasonCode; TryParse returns bool; the low-level TryParse(span, out value, out ParseDetail) returns OperationStatus and a detail with issues, field offsets, confidences and whether a rule or a model decided each field.
  • Offsets. Every value and part carries a SourceSpan of UTF-16 offsets into the original input.
  • Never invented. OneContact does not fill in a city from a postcode, guess gender or origin, or apply a typo fix. Typos come back as suggestions (EmailAddress.TypoSuggestion), facts as flags.

libphonenumber (Java) and libphonenumber-csharp → OneContact

Both libraries expose one PhoneNumberUtil singleton with the same method names (the C# port in Pascal case) and a mutable PhoneNumber message. OneContact has an immutable PhoneNumber struct with the common questions as members, and a PhoneNumberParser engine class for everything that takes a metadata choice. Namespaces: RecursiveAI.OneContact.Phones for the engine, enums and matcher; RecursiveAI.OneContact for PhoneNumber and Region. Regions are a Region struct (Region.US, Region.Parse("DE")), not strings.

libphonenumberOneContact
PhoneNumberUtil.getInstance()PhoneNumberParser.Default (most calls are also members of PhoneNumber)
parse(text, region)PhoneNumber.Parse(text, Region), PhoneNumber.TryParse(text, Region, out value)
parse of an international number (+...), region ZZ or nullPhoneNumber.Parse(text) (no region)
parseAndKeepRawInputPhoneNumberParser.TryParse(span, region, keepRawInput: true, out number, out detail); number.RawInput, number.CountryCodeSource
NumberParseException and its ErrorTypeContactParseException.Reason: PhoneRegionRequired, PhoneNotANumber, PhoneInvalidCountryCode, PhoneTooShortAfterIdd, PhoneTooShort, PhoneTooLong (ids such as phone.too-short)
isValidNumbernumber.IsValid
isValidNumberForRegionnumber.IsValidForRegion(region)
isPossibleNumber / isPossibleNumberWithReasonnumber.GetPossibility() returns PhonePossibility (IsPossible, IsPossibleLocalOnly, TooShort, TooLong, InvalidLength, InvalidCountryCode); GetPossibility(type) checks one number type
getNumberTypenumber.Type (PhoneNumberType)
getRegionCodeForNumbernumber.Region
getRegionCodesForCountryCode, getCountryCodeForRegionPhoneMetadata.Default.GetRegions(callingCode), GetCallingCode(region)
getSupportedRegions, getSupportedCallingCodesPhoneMetadata.Default.Regions, .CallingCodes
getSupportedTypesForRegionPhoneNumberParser.Default.GetSupportedTypes(region)
getNationalSignificantNumbernumber.NationalSignificantNumber
getCountryCode, getNationalNumber, hasExtension, getExtensionnumber.CountryCode, .NationalNumber, .Extension (null when absent)
isItalianLeadingZero, getNumberOfLeadingZerosnumber.ItalianLeadingZero, .LeadingZeroCount
format(number, E164 / INTERNATIONAL / NATIONAL / RFC3966)number.Format(PhoneFormat.E164 / International / National / Rfc3966); number.E164
formatOutOfCountryCallingNumbernumber.FormatOutOfCountry(callingFrom)
formatNumberForMobileDialingnumber.FormatForMobileDialing(callingFrom, withFormatting)
formatInOriginalFormatnumber.FormatInOriginalFormat(callingFrom) (returns the raw input where formatting would change a digit)
formatNationalNumberWithCarrierCode, ...WithPreferredCarrierCodePhoneNumberParser.Default.FormatNationalWithCarrierCode, FormatNationalWithPreferredCarrierCode
formatOutOfCountryKeepingAlphaChars, formatByPatternno equivalent (vanity letters are converted when parsing; custom patterns are not supported)
getExampleNumber, getExampleNumberForTypePhoneNumber.GetExample(region, type) (null when the region has none)
getExampleNumberForNonGeoEntityPhoneNumberParser.Default.GetNonGeographicExample(callingCode)
isAlphaNumberPhoneNumberParser.IsVanityNumber(text); PhoneFacts.Vanity
getLengthOfGeographicalAreaCode, getLengthOfNationalDestinationCodeGetAreaCodeLength(number), GetNationalDestinationCodeLength(number)
isNumberGeographical, canBeInternationallyDialledIsGeographical(number), CanBeInternationallyDialled(number)
getNddPrefixForRegionGetNationalPrefix(region)
isNANPACountry, isMobileNumberPortableRegionIsNorthAmericanPlan(region), IsMobileNumberPortableRegion(region)
isNumberMatch and MatchTypeno equivalent: compare number == other (canonical form) or number.Key (E.164 plus extension)
findNumbers(text, region) and PhoneNumberMatcherPhoneNumber.FindAll(text, region, leniency); the engine form PhoneNumberMatcher.Default.FindAll(...) / TryFindNext(...) on spans; matches carry Start, End, RawString, Number
Leniency.POSSIBLE / VALID / STRICT_GROUPING / EXACT_GROUPINGPhoneMatchLeniency.Possible / Valid / StrictGrouping / ExactGrouping
AsYouTypeFormatter (getAsYouTypeFormatter(region), inputDigit, inputDigitAndRememberPosition, getRememberedPosition, clear)new AsYouTypeFormatter(region), InputDigit(char), InputDigitAndRememberPosition(char), RememberedPosition, Clear()
ShortNumberInfo isPossibleShortNumber, isValidShortNumberShortNumberInfo.Default.IsPossible(number), .IsValid(number) and the ...ForRegion(number, region) forms
getExpectedCost, ShortNumberInfo.ShortNumberCostGetExpectedCost(number), GetExpectedCostForRegion(number, region); ShortNumberCost
isEmergencyNumber, connectsToEmergencyNumberIsEmergencyNumber(text, region), ConnectsToEmergencyNumber(text, region)
isCarrierSpecific, isSmsServiceForRegion, getExampleShortNumberIsCarrierSpecific, IsSmsServiceForRegion, GetExample(region)
PhoneNumberOfflineGeocoder.getDescriptionForNumber(number, locale)PhoneGeocoder.Default.GetDescription(number, "en") (a BCP 47 tag, package RecursiveAI.OneContact.Phones.Geo)
PhoneNumberToCarrierMapper.getNameForNumber, getSafeDisplayNamePhoneCarrierMapper.Default.GetName(number, tag), .GetSafeDisplayName(number, tag); the carrier is the one a range was first assigned to, so read the caveat in phones-geo.md
PhoneNumberToTimeZonesMapper.getTimeZonesForNumberPhoneTimeZoneMapper.Default.GetTimeZones(number)
Loading other metadata (MetadataLoader, a replaced metadata file)new PhoneNumberParser(PhoneMetadata.LoadXml(stream)), which reads the same XML format with size limits; see phones.md
A PhoneNumber as a dictionary key or duplicate checknumber.Key (E.164 plus extension) or Contacts.SameKey

Concepts that differ:

  • Metadata is compiled into the package from libphonenumber's published metadata files at a pinned release; phones.md says which. A newer release arrives as a patch release, or load it yourself with PhoneMetadata.LoadXml.
  • Offsets. number.Source is the span of the number inside the text you parsed; matcher results carry Start and End.
  • Batch. PhoneNumberParser.ParseMany(inputs, region) parses a sequence without exceptions.

libpostal → OneContact

libpostal gives two calls: parse_address (free text to labelled components) and expand_address (free text to normalized variants), plus near-duplicate hashing. OneContact folds them into PostalAddress (parse, standardize, render, validate, key) and AddressExpander. The big differences: OneContact reads rules first; in 1.0 that is all it does, and a text the rules cannot read is refused with a reason (small model packages for what the rules leave open follow in 1.1), it has no multi-gigabyte data directory, and it renders addresses back in each national postal authority's own style.

libpostalOneContact
parse_address(text) with optional language and country optionsPostalAddress.Parse(text); with a country, PostalAddress.TryParse(text, Region, out value, out detail). Without a country OneContact infers it from the evidence in the text (Flags has CountryInferred) or reports CountryUndetermined; it never guesses
Components come back as (label, value) pairsProperties on PostalAddress; offsets and confidences through ParseDetail.Fields and PostalAddress.RuleDetail
house (a venue or building name)Organization / Recipient lines
house_numberStreetDetail part StreetPartRole.HouseNumber
roadStreetDetail parts PreDirectional, StreetName, StreetType, PostDirectional; the whole line is in StreetLines
unitparts UnitDesignator and UnitNumber
po_boxstreet line of kind PostOfficeBox with parts BoxDesignator, BoxNumber (AddressFacts.PostOfficeBox)
level, staircase, entrancenot separate fields; the text stays in the street line as written
suburb, city_districtDependentLocality
cityLocality
state_district, state, country_regionAdminArea (the authority's state, province or territory code is the standard form)
postcodePostalCode
countryCountry (a Region)
category, near, island, world_regionnot fields
expand_address(text, languages=[...])AddressExpander.Expand(text, ExpansionLanguages.English | ExpansionLanguages.German, maxVariants); the languages are flags (English, French, German, Dutch, Italian, Luxembourgish, Spanish, Portuguese, Catalan, Basque, Galician, Maltese, Irish; English and French by default)
Expansion options (which transformations to apply)no per-transformation switches: the variants come back with the fully expanded one first, capped by maxVariants (default 16)
Near-duplicate hashing and duplicate classificationPostalAddress.Key / GetKey(KeyVersion): one deterministic string built from the parsed parts and the expansions; equal keys mean the same address under the documented rules (see address-keys.md). There is no probabilistic classifier
libpostal_setup() and the data directory (about 2 GB)nothing to set up; rules and dictionaries are compiled in (the core package is 1.4 MB); the model packages of 1.1 will be 10 to 23 MB
Language classification (libpostal_place_languages)not a call: the reader chooses languages from the country and the evidence
Normalizing for display or labelsaddress.ToString(AddressStyle.Postal / Display, AddressLayout.Label / SingleLine, includeCountry); GetLabelLines() and ToSingleLine() for the country's own field layout
Checking an address is plausibleaddress.Validate() returns AddressIssues from structural rules only (required fields, postcode pattern, postcode and region agreement). There are no reference-data lookups

Not equivalent: libpostal's training data and models are not used or read. The OneContact models (from 1.1) are trained on open registers, our own renderers and, for the default Models package, OpenStreetMap-derived addresses, with their provenance in the model packages.

python-nameparser and probablepeople → OneContact

python-nameparser (HumanName) splits a person name by rules, with customizable lists. probablepeople tags a person or corporation string with a trained CRF. OneContact does both jobs in PersonName, CompanyName and NameParsing: rules first, with the decision source reported (a small model for the unclear remainder follows in 1.1).

python-nameparserOneContact
HumanName(full_name)PersonName.Parse(text); add NameHints.ForCountry(Region) or NameHints.ForLanguage(tag) for order and particles
name.titlePrefix
name.firstGiven
name.middleMiddle
name.lastFamily, with Particle kept separate (van der) and SecondFamily for Iberian names
name.suffixSuffix (generational) and ProfessionalSuffix (credentials)
name.nicknameNickname
name.full_name, str(name), string_formatCanonical; compose your own text from the parts
name.capitalize()done on parse: all-caps or all-lower input is restored (NameFacts.CasingRestored), traceable by offsets
name.original, name.unparsableSource span; TryParse returns false or an OperationStatus with a reason
name.as_dict(), initials()read the parts; no initials helper
"Last, First" inputread as given order FamilyCommaGiven (PersonName.Order)
Customizing CONSTANTS (titles, prefixes, suffixes, conjunctions)no runtime mutation of the lists. Titles, suffixes, particles and legal forms are compiled data; use NameHints for country and language
"John and Jane Smith" (kept as one string)NameParsing.Split(text) returns separate PersonNames that share the family name
Equality of names== compares canonical forms; person.Key is the aggressive key (folded diacritics, nickname equivalence, titles and suffixes dropped, see name-keys.md)
probablepeopleOneContact
tag(text) returning Person, Corporation or HouseholdNameParsing.Classify(text, hints) / NameParsing.Split(text, hints).Class: NameClass.Person, Company, Multiple (a household of several people), Unknown. Unknown is an honest answer
parse(text) returning (token, label) pairsthe parts of PersonName and CompanyName, each with a span
GivenName, MiddleName, SurnameGiven, Middle, Family
FirstInitial, MiddleInitial, LastInitialno separate label: an initial is a Given, Middle or Family part whose text is one letter
PrefixMarital, PrefixOtherPrefix
SuffixGenerational, SuffixOtherSuffix, ProfessionalSuffix
NicknameNickname
Anda split into several people (NameClass.Multiple)
CorporationName, CorporationNameOrganizationCompanyName.Base
CorporationLegalTypeCompanyName.LegalForm: an ISO 20275 entity legal form with its ELF code (ElfCode) and canonical abbreviation; LegalFormBeforeName, LegalFormAmbiguous and LegalFormInactive flags
Other corporation labels (branch, committee, ...)not parts. DBA and c/o are split out as TradingAs and CareOf; a leading "The" is LeadingArticle
Model confidenceParseDetail per field: Confidence and Source (Rule or Model)
Training your own modelfrom 1.1: the recipes in recipes/ use the Train API of OneNLP; see models.md

Not equivalent: neither library's data or models are used. OneContact never infers gender, origin or a salutation from a name.