# expostal is an Elixir NIF that needs libpostal installed system-wide

> A thin binding to the Libpostal C library, two functions, and a Makefile that computes a libpostal path it never uses. Returned values are lowercased, expand_address gives you a list, and the last commit was 2023-07-23.

**SweetIQ/expostal** — Elixir binding for Libpostal - a library for parsing/normalizing street addresses around the world. Powered by statistical NLP and open geo data.

- Repository: https://github.com/SweetIQ/expostal
- Stars: 100 · Forks: 21
- Language: Elixir
- License: MIT
- Published: 2026-08-17 · Updated: 2026-08-18 · Language: en
- Canonical page: https://hysenlabs.com/projects/sweetiq-expostal

## The hard dependency is a C library, not the hex package

The Installation section is four lines, and they only cover half of what you need.

The package is added the usual Elixir way, by putting it in the dependency list in `mix.exs`:

```elixir
def deps do
  [{:expostal, "~> 0.2.0"}]
end
```

Then the Dependencies section says the package depends on a system-wide installation of libpostal, and links the Libpostal installation instructions. So the version constraint `~> 0.2.0` is only half the setup: before `mix compile` can link anything, the C library has to be present on the machine with headers the compiler can find.

That is the whole of the installation story, and it is a meaningful one. A hex dependency is normally self-contained; this one is a native extension over a data-heavy C library that has to be installed, configured and located separately. Everything that follows, in the build file and in the deployment, follows from that one line.

The page also links a tutorial on writing Elixir and Erlang NIFs, which tells you the approach: this is a native interface function, not a port or an HTTP client.

## make runs mix compile, and the .so is a separate rule

The build is driven by a hand-written Makefile, and reading it saves a lot of guessing.

The default target is `all`, which depends on a target named `libpostal`, whose recipe is `$(MIX) compile`. So running `make` compiles the Mix project and nothing else. The target is named after the C library it links against, which is misleading: no part of the default path installs, builds or checks libpostal.

The actual shared object has its own rule:

```
priv/expostal.so: src/expostal.c
	$(CC) $(CFLAGS) -shared $(LDFLAGS) -o $@ src/expostal.c
```

Nothing in the default path depends on `priv/expostal.so`, so whether the NIF is actually built as part of `mix compile` is left to the Mix build configuration rather than to this Makefile.

Two details in that rule are worth noticing. The output name is `.so` unconditionally, even though the file contains a `Windows_NT` check elsewhere, so a Windows build produces a file with a Unix extension. And the `clean` target runs `mix clean` followed by removing everything under `priv/`, so it is broader than a Mix clean and will empty that directory.

## LIBPOSTAL_PATH is computed in two branches and never used

This is the sharpest thing in the build file, and it is the first thing that will bite anyone building from a checkout.

There is a conditional that tests whether a vendored copy exists:

```
ifeq ($(wildcard deps/libpostal),)
	LIBPOSTAL_PATH = ../libpostal
else
	LIBPOSTAL_PATH = deps/libpostal
endif
```

So the variable resolves to `../libpostal` when there is no vendored copy and to `deps/libpostal` when there is. Then it is never referenced again anywhere in the file. Neither branch reaches `CFLAGS`.

What does reach the compiler is this:

```
CFLAGS += -I/usr/local/include -I/usr/include -L/usr/local/lib -L/usr/lib
CFLAGS += -lpostal
```

Four hardcoded paths. So a system-wide libpostal installed under `/usr/local` or `/usr` works, and the vendored-checkout path in the conditional is decorative. If your libpostal lives anywhere else, or if you took the vendored route expecting that variable to be picked up, the build fails at link time with no obvious cause.

The compiler flags themselves are conservative and sensible: `-O3`, `-Wall -Wextra`, `-Wno-unused-parameter`, `-std=gnu99` and `-Wno-unused-function`.

## The Erlang include path is derived by running erl at build time

One line in the Makefile does something unusual enough to be worth reading twice.

`ERLANG_PATH` is not a constant. It is the output of a shell invocation of `erl` with an `-eval` expression that concatenates `code:root_dir()`, the literal string `erts-`, and `erlang:system_info(version)`, then adds `/include`. So the include directory handed to the C compiler is derived from the Erlang installation on the machine doing the build, at build time.

That is the right approach in principle, since the `erts` include directory genuinely moves with the OTP version. It also means the build starts an Erlang VM. If `erl` is not on the path, or points at a different installation than the one Mix is using, the include path is wrong and the compile fails on missing BEAM headers.

The platform handling is where the NIF mechanics show. When the OS is not `Windows_NT`, `-fPIC` is added, and on Darwin specifically the linker flags become `-dynamiclib -undefined dynamic_lookup`. That combination is what lets the shared object resolve BEAM symbols when it is loaded rather than requiring them to be present at link time, which is how a NIF behaves.

The rest of the flags include `-ansi -pedantic`, which is unusual next to `-std=gnu99` later in the same variable.

## parse_address returns lowercase components, expand_address returns a list

Two functions, two different shapes of answer, and one behaviour worth knowing before you write the formatter.

Parsing:

```
iex> Expostal.parse_address("615 Rene Levesque Ouest, Montreal, QC, Canada")

%{city: "montreal", country: "canada", house_number: "615",
  road: "rene levesque ouest", state: "qc"}
```

A map with five keys, and every value is lowercased. The input had `Montreal` and `QC`, and the output is `montreal` and `qc`. Libpostal normalises, so if you need the display casing for an address form you have to re-case it yourself, and if your database compares case-sensitively these values will not match what your users typed.

Expanding is the opposite shape. It returns a list of candidate strings:

```
iex> Expostal.expand_address("781 Franklin Ave Crown Hts Brooklyn NY")

["781 franklin avenue crown heights brooklyn new york",
  "781 franklin avenue crown heights brooklyn ny"]
```

Two variants that differ only in whether the state is spelled out or abbreviated. That is the intended use: feed the list to a geocoder and take whichever candidate resolves.

## Travis CI, a travis-ci.org badge, and a provisioning script

The repository's automation is visible in three places, and all three are from one era.

The badge at the top of the page points at `travis-ci.org/SweetIQ/expostal`, the older of the two Travis domains. The tree holds a `.travis.yml` for the build definition and a `.provision.travis.sh` for provisioning the machine it runs on, which in this case means getting libpostal installed before the build can link.

That provisioning script is the more interesting of the two files, because it is the only place the libpostal setup is automated. For anyone cloning this today, the dependency that has to be system-wide is set up by a script written for a CI provider's hosted image, and it has not been revisited since.

The rest of the tree is a conventional Elixir project: `mix.exs`, `mix.lock`, `lib/` for the Elixir modules, `src/expostal.c` for the NIF source, `priv/` where the shared object is built, `config/`, `test/`, a `Makefile`, `LICENSE`, and a `.vscode/` directory.

Documentation is published on hexdocs.pm and can be generated locally with `mix docs`, which is the standard Elixir arrangement.

## The last push was 2023-07-23 and there are no releases

The maintenance picture is the thing to weigh before you adopt this, and it is not a picture of an actively worked-on package.

The last push to the `master` branch was on 2023-07-23. The repository publishes no GitHub releases, so there is no tag to pin and nothing to diff between versions. It is not archived, and the licence is MIT.

The realistic risk is not abandonment so much as drift in the thing it binds to. Elixir NIFs break when the BEAM or the build tooling moves underneath them, and this one has a 2023 toolchain: a Makefile with `-ansi -pedantic` next to `-std=gnu99`, Travis as the CI, a version constraint of `~> 0.2.0`, and a libpostal whose current version nobody in this repository has checked against.

What you get for that is a very small, very clear surface. Two functions, one map, one list, and no HTTP client, no port and no background process between your Elixir code and the address parser. If you already have libpostal on the machine, that is the whole value, and it is a reasonable thing to want.

The alternative shape is not another library but a different architecture: skip the NIF and call out to a service or a port, which removes the system-wide C dependency and the native build entirely at the cost of a process or network boundary. Which one is better depends on whether you already run libpostal, not on which wrapper is newer.

## Conclusion

expostal fits an Elixir application that has Libpostal installed system-wide already, or can install it, and wants address parsing and expansion in the same process as the rest of the BEAM with no network hop. It does not fit a deployment that cannot take a system-wide C dependency, or anyone who expects more than two functions. Verify four things before you depend on it: that libpostal's headers sit in `/usr/local/include` or `/usr/include`, since the include paths the build uses are hardcoded and the computed `LIBPOSTAL_PATH` never reaches the compiler; that `mix compile` is what actually builds `priv/expostal.so`, since the Makefile's default target only runs `mix compile`; whether your consumers can live with lowercase output from `parse_address`; and whether `~> 0.2.0` still resolves for you, given the last push was on 2023-07-23 and there are no releases.

## FAQ

### How do I add expostal to an Elixir project?

Add {:expostal, "~> 0.2.0"} to the deps list in mix.exs. Documentation is published on hexdocs.pm/expostal, or you can generate it locally with mix docs.

### What does expostal need installed on the system?

A system-wide installation of libpostal, the C library it binds. The page describes it as a hard dependency and links the libpostal installation instructions, so adding the hex dependency alone is not enough for the build to link.

### What does Expostal.parse_address return?

A map with city, country, house_number, road and state. The example returns lowercase values, so Montreal and QC come back as montreal and qc rather than in their input casing.

### What does Expostal.expand_address return?

A list of expanded address strings. Given 781 Franklin Ave Crown Hts Brooklyn NY, the example returns two variants that differ only in whether the state is spelled out or left as the abbreviation.

### Is expostal still being worked on?

The last push to the master branch was on 2023-07-23 and no GitHub releases are published. Its CI is Travis, with a badge pointing at travis-ci.org and a .provision.travis.sh script that installs libpostal first.

## Sources

- [Official README](https://github.com/SweetIQ/expostal#readme)
- [Project repository](https://github.com/SweetIQ/expostal)
- [README](https://github.com/SweetIQ/expostal/blob/master/README.md)
- [Releases](https://github.com/SweetIQ/expostal/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/sweetiq-expostal
