.NET bindings for libpostal, the international street-address parser and normalizer powered by statistical NLP.
SharpPostal lets you call libpostal's two core capabilities from C#:
- Expansion — turn
"120 E 96th St"into normalized variants like"120 east 96th street"and"120 east 96 street", useful for fuzzy address matching and deduplication. - Parsing — break
"The Book Club 100-106 Leonard St Shoreditch London EC2A 4RH"into labeled components (house_number,road,city,postcode, etc.).
If you've just cloned the repo on a fresh machine:
# 1. Build libpostal for Windows by following the upstream guide:
# https://github.com/openvenues/libpostal#installation-windows
# Then copy the .dll (libpostal-1.dll) into:
# src\runtimes\win-x64\native\
# 2. Build and run the demo
dotnet build SharpPostal.sln
dotnet run --project demo- Prerequisites
- Install the native library
- Install the data files
- Add SharpPostal to your project
- Usage
- Threading and lifecycle
- Troubleshooting
- .NET 10 SDK or later — download here.
- Windows x64. The project ships native binaries for
win-x64. Other platforms work in principle (libpostal builds on Linux/macOS), but you'll need to supply the matching native binaries and addruntimes/<rid>/native/folders for them. - ~3 GB free disk space for the model data, plus another ~2 GB of RAM at runtime once everything is loaded.
- An archive tool that handles
.tar.gz(7-Zip, WinRAR, the built-intaron Windows 10+).
Follow the official libpostal Windows installation guide:
https://github.com/openvenues/libpostal#installation-windows
It walks through installing MSYS2/MinGW, cloning libpostal, and running ./bootstrap.sh && ./configure && make && make install. When that's done you'll have a fully built and installed copy of libpostal on your machine, with the native DLL in a folder. Data files are installed when going through the process!
When the upstream install finishes, you'll have a folder containing libpostal-1.dll
Copy libpostal-1.dll into:
src\runtimes\win-x64\native\
After copying, the layout should look something like:
src\runtimes\win-x64\native\
└── libpostal-1.dll
If the build later fails with DllNotFoundException, this command tells you what libpostal-1.dll is trying to load:
dumpbin /dependents src\runtimes\win-x64\native\libpostal-1.dllStandard Windows DLLs (KERNEL32.dll, msvcrt.dll, ucrtbase.dll, api-ms-win-*, ADVAPI32.dll) are always present on Windows. Anything else listed must live in src\runtimes\win-x64\native\ next to libpostal-1.dll.
dotnet add <YourProject>.csproj reference path\to\SharpPostal\src\SharpPostal.csprojThe native DLL is copied to the output directory automatically. After building, you should see libpostal-1.dll sitting next to your .exe.
libpostal has three independent data subsystems. Load whichever ones you'll use:
using SharpPostal;
const string DataDir = @"C:\libpostal_data";
// For ExpandAddress without explicit language hints
LibPostal.InitializeFromDirectory(DataDir);
LibPostal.InitializeLanguageClassifierFromDirectory(DataDir);
// For ParseAddress
LibPostal.InitializeParserFromDirectory(DataDir);Each initialization call takes a few seconds and allocates significant memory. Do this once per process, ideally during app startup. If you call them more than once they're no-ops, so they're safe to call from multiple places defensively.
If you prefer the path baked into the DLL at compile time (typically C:\libpostal on Windows builds), use the parameterless versions instead: Initialize(), InitializeLanguageClassifier(), InitializeParser(). The directory-based versions are generally more portable.
LibPostal.Shutdown();This releases all model memory. Optional but tidy.
string[] variants = LibPostal.ExpandAddress("120 E 96th St New York");
foreach (var v in variants)
Console.WriteLine(v);
// 120 east 96th street new york
// 120 east 96 street new york
// ...If you know the language ahead of time, pass it. This skips the language classifier (which means you don't even need to call InitializeLanguageClassifier) and is significantly faster on tight loops:
string[] variants = LibPostal.ExpandAddress(
"Friedrichstraße 123, 10117 Berlin",
languages: new[] { "de" });var components = LibPostal.ParseAddress(
"The Book Club 100-106 Leonard St Shoreditch London EC2A 4RH United Kingdom");
foreach (var (label, value) in components)
Console.WriteLine($"{label,-15} {value}");
// house the book club
// house_number 100-106
// road leonard st
// suburb shoreditch
// city london
// postcode ec2a 4rh
// country united kingdomOptionally pass language and country hints to bias the parser:
var components = LibPostal.ParseAddress(
"Friedrichstraße 123, 10117 Berlin",
language: "de",
country: "DE");using System;
using SharpPostal;
const string DataDir = @"C:\libpostal_data";
Console.WriteLine("Initializing libpostal (this takes a few seconds)...");
LibPostal.InitializeFromDirectory(DataDir);
LibPostal.InitializeLanguageClassifierFromDirectory(DataDir);
LibPostal.InitializeParserFromDirectory(DataDir);
try
{
var expansions = LibPostal.ExpandAddress("120 E 96th St New York");
Console.WriteLine($"Got {expansions.Length} variants");
var parsed = LibPostal.ParseAddress("781 Franklin Ave Crown Hts Brooklyn NY 11216");
foreach (var (label, value) in parsed)
Console.WriteLine($" {label,-15} {value}");
}
finally
{
LibPostal.Shutdown();
}| Member | Purpose |
|---|---|
LibPostal.Initialize() / InitializeFromDirectory(path) |
Load expansion model data. Required for ExpandAddress. |
LibPostal.InitializeLanguageClassifier() / InitializeLanguageClassifierFromDirectory(path) |
Load language detection model. Required for ExpandAddress unless you pass explicit languages. |
LibPostal.InitializeParser() / InitializeParserFromDirectory(path) |
Load address parser model. Required for ParseAddress. |
LibPostal.Shutdown() |
Release all loaded model data. |
LibPostal.ExpandAddress(string address, IReadOnlyList<string>? languages = null) |
Returns string[] of normalized variants. |
LibPostal.ParseAddress(string address, string? language = null, string? country = null) |
Returns AddressComponent[] of (Label, Value) pairs. |
AddressComponent |
readonly record struct with Label and Value properties. |
- Initialization is not thread-safe. Call
Initialize*methods from one thread (typically app startup) before any other thread starts using libpostal. The wrapper protects against double-init withInterlockedflags, but the underlying C library still expects single-threaded setup. ExpandAddressandParseAddressare safe to call concurrently from many threads once setup is complete. The models are read-only after loading.- Model data is process-global, not per-instance. You cannot have two configurations of libpostal in the same process.
- Memory usage is significant. Plan for ~2 GB resident once everything is loaded. The parser alone is the largest single component.
SharpPostal/
├── SharpPostal.sln Solution file
├── src/
│ ├── SharpPostal.csproj Binding library project
│ ├── AssemblyInfo.cs [assembly: DisableRuntimeMarshalling]
│ ├── Native.cs P/Invoke declarations (internal)
│ ├── LibPostal.cs High-level public API
│ ├── Utf8.cs UTF-8 marshalling helpers
│ └── runtimes/win-x64/native/
│ └── libpostal-1.dll Native library
└── demo/
├── SharpPostal.Demo.csproj
└── Program.cs Sample console app
This binding code is released under the MIT license. The native libpostal library is itself MIT-licensed; see its repo for details. The data files are derived from OpenStreetMap and other open data; refer to libpostal's documentation for attribution requirements when redistributing them.