Parsing addresses with libpostal

libpostal is an open-source library that splits an address into its parts, such as the house number, street, city, and postcode, and normalizes it, so that 128 SW 3rd Ave and 128 Southwest Third Avenue can be matched. It was trained on more than a billion addresses from OpenStreetMap and OpenAddresses, and handles addresses from every country without rules for each one.

It isn't a geocoder: it reads the text, and doesn't know where the address is or whether it exists. This guide installs it, parses and matches addresses in Python, runs it as a web service, and covers when a geocoding API does the job instead.

It was tested with libpostal built from its repository in October 2026, the postal Python package 1.1.11 on Python 3.13, and the pelias/libpostal-service Docker image.

Install libpostal

libpostal is a C library, built from source. On Ubuntu or Debian, install the build tools, then build it:

sudo apt-get install -y curl build-essential autoconf automake libtool pkg-config
git clone https://github.com/openvenues/libpostal
cd libpostal
./bootstrap.sh
./configure --datadir=/usr/local/share
make -j4
sudo make install
sudo ldconfig

make also downloads the trained model, about 2 GB, into the --datadir folder. On a Mac with Apple silicon, add --disable-sse2 to ./configure. The README covers Windows, and other ways to install it.

Then install the Python package, which uses the library you just built:

pip install postal

Parse an address

parse_address returns each part of the address with a label:

from postal.parser import parse_address

parse_address("apt 4b 350 5th ave new york ny 10118")
[
    ('apt 4b', 'unit'),
    ('350', 'house_number'),
    ('5th ave', 'road'),
    ('new york', 'city'),
    ('ny', 'state'),
    ('10118', 'postcode'),
]

It works the same way for addresses written in other orders and languages:

parse_address("Rua Augusta 274, 1100-053 Lisboa, Portugal")
[
    ('rua augusta', 'road'),
    ('274', 'house_number'),
    ('1100-053', 'postcode'),
    ('lisboa', 'city'),
    ('portugal', 'country'),
]

The parts come back in lowercase, in the order they were written. The common labels are house for a building or venue name, house_number, road, unit, level, po_box, postcode, suburb, city_district, city, state_district, state, and country. The README lists them all.

To turn the list into a dictionary of fields, such as for a database or a form:

fields = {label: value for value, label in parse_address(address)}

Match addresses written different ways

expand_address normalizes an address into the forms it could be written in, expanding abbreviations such as SW and Ave:

from postal.expand import expand_address

expand_address("128 SW 3rd Ave Portland")
[
    '128 southwest 3rd avenue portland',
    '128 sw 3rd avenue portland',
    '128 southwest 3 avenue portland',
    '128 sw 3 avenue portland',
]

Two addresses are likely the same if their expansions share a string:

def same_address(a, b):
    return bool(set(expand_address(a)) & set(expand_address(b)))


same_address("128 Southwest Third Avenue, Portland", "128 SW 3rd Ave Portland")  # True

This is how libpostal helps remove duplicates from a list of addresses, or match records from two systems that format addresses differently.

Run it as a web service

Loading the model takes about 2 GB of memory and, on the test machine, 12 seconds. That's fine for a long-running process, but slow for a script that starts often, and it needs a binding in your language. Running libpostal as a service loads the model once and answers over HTTP. The Pelias project publishes one as a Docker image, with the model included:

docker run -d -p 4400:4400 pelias/libpostal-service
curl "http://localhost:4400/parse?address=apt+4b+350+5th+ave+new+york+ny+10118"
[
  { "label": "unit", "value": "apt 4b" },
  { "label": "house_number", "value": "350" },
  { "label": "road", "value": "5th ave" },
  { "label": "city", "value": "new york" },
  { "label": "state", "value": "ny" },
  { "label": "postcode", "value": "10118" }
]

/expand?address=… returns the normalized forms. The image is 3.5 GB to download, and the running service used 1.9 GB of memory.

Other languages

Besides Python, libpostal has official bindings for Node.js, Go, Java, Ruby, and PHP. Each needs the C library installed as above. The web service works from any language.

Things to know

  • It reads text, and doesn't check it. A made-up address parses as well as a real one, and libpostal can't say where it is.
  • It's fast once loaded. After the 12-second start, parsing took about 0.2 milliseconds an address on the test machine.
  • Names can look like places. A landmark name on its own, such as Colosseum, can be read as a city.
  • There's an alternative model. Building with ./configure … MODEL=senzing uses a model from Senzing, about 2.2 GB, which parses US, UK, and Singapore addresses better.

Parse and geocode in one step

To get coordinates, an address has to be geocoded, not only parsed. Mapricot's search runs on Pelias, which uses libpostal to read each query, so you can send the whole address as it's written. Each result comes back with its coordinates and its address in parts:

Request
curl "https://api.mapricot.com/v1/search?q=128+SW+3rd+Ave,+Portland&limit=1" \
  -H "Authorization: Bearer $MAPRICOT_KEY"
Response
{  "attribution": [    {      "source": "openaddresses",      "text": "OpenAddresses",      "url": "https://openaddresses.io"    }  ],  "results": [    {      "id": "mp_b3BlbmFkZHJlc3NlczphZGRyZXNzOnVzL29yL211bHRub21haDpkZjdlYzM3YWJiYTJiNWM0",      "type": "address",      "name": "128 SW 3rd Avenue",      "label": "128 SW 3rd Avenue, Portland, OR, USA",      "lat": 45.521736,      "lon": -122.673276,      "address": {        "houseNumber": "128",        "street": "SW 3rd Avenue",        "postalCode": "97204",        "locality": "Portland",        "region": "Oregon",        "regionCode": "OR",        "country": "United States",        "countryCode": "US"      },      "source": "openaddresses"    }  ]}

The parts come from the matched address, not from the text sent, so they're spelled out and capitalized the way the data has them. The first 250,000 requests every 30 days are free, then they cost $0.10 per 1,000. For a whole file of addresses, see batch geocoding a CSV file.

Questions

Is libpostal free? Yes. It's open source under the MIT license, and its model is free to download.

Does libpostal validate addresses? No. It splits text into parts whether or not the address exists. To check an address, geocode it and look at how exactly it matched.

Why does libpostal need so much memory? The trained model, with the abbreviations and place names for every country, is loaded into memory to parse quickly. It takes about 2 GB.

Can I use libpostal without compiling it? The pelias/libpostal-service Docker image above has it built, and answers over HTTP. Otherwise the Python and other bindings need the C library built first.