first commit

This commit is contained in:
William Dillon 2026-08-08 08:50:11 -04:00
commit d492948756
6 changed files with 301 additions and 0 deletions

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# libpostal
libpostal is a cgo wrapper over the libpostal lib for parsing postal addresses.
## requirements
libpostal shared library

38
address.go Normal file
View File

@ -0,0 +1,38 @@
package libpostal
import (
"fmt"
"strings"
)
type Address struct {
HouseNumber string
Road string
City string
State string
PostCode string
}
func (a *Address) Invalid() error {
var missingFields []string
if len(a.HouseNumber) == 0 {
missingFields = append(missingFields, "HouseNumber")
}
if len(a.Road) == 0 {
missingFields = append(missingFields, "Road")
}
if len(a.City) == 0 {
missingFields = append(missingFields, "City")
}
if len(a.State) == 0 {
missingFields = append(missingFields, "State")
}
if len(a.PostCode) == 0 {
missingFields = append(missingFields, "PostCode")
}
if len(missingFields) > 0 {
return fmt.Errorf("missing fields: %s", strings.Join(missingFields, ", "))
}
return nil
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module code.wmdillon.com/wmdillon/libpostal
go 1.25

135
libpostal.go Normal file
View File

@ -0,0 +1,135 @@
/*
The functions in this file are unexported to ensure safe use through the Parser.
*/
package libpostal
//#cgo LDFLAGS: -lpostal
//#include <libpostal/libpostal.h>
import "C"
import (
"fmt"
"strings"
"unsafe"
)
func setup() bool {
response := C.libpostal_setup()
return bool(response)
}
func setupDataDir(datadir string) bool {
datadir_cstr := C.CString(datadir)
response := C.libpostal_setup_datadir(datadir_cstr)
C.free(unsafe.Pointer(datadir_cstr))
return bool(response)
}
func tearDown() {
C.libpostal_teardown()
}
func setupParser() bool {
response := C.libpostal_setup_parser()
return bool(response)
}
func setupParserDataDir(datadir string) bool {
datadir_cstr := C.CString(datadir)
response := C.libpostal_setup_parser_datadir(datadir_cstr)
C.free(unsafe.Pointer(&datadir_cstr))
return bool(response)
}
func tearDownParser() {
C.libpostal_teardown_parser()
}
func setupLanguageClassifier() bool {
response := C.libpostal_setup_language_classifier()
return bool(response)
}
func setupLanguageClassifierDataDir(datadir string) bool {
datadir_cstr := C.CString(datadir)
response := C.libpostal_setup_language_classifier_datadir(datadir_cstr)
C.free(unsafe.Pointer(datadir_cstr))
return bool(response)
}
func tearDownLanguageClassifier() {
C.libpostal_teardown_language_classifier()
}
type AddressParserOptions struct {
Language string
Country string
}
func (goOpts AddressParserOptions) toC() (C.libpostal_address_parser_options_t, func()) {
cOpts := C.libpostal_get_address_parser_default_options()
var allocations []unsafe.Pointer
if goOpts.Language != "" {
cLang := C.CString(goOpts.Language)
cOpts.language = cLang
allocations = append(allocations, unsafe.Pointer(cLang))
}
if goOpts.Country != "" {
cCountry := C.CString(goOpts.Country)
cOpts.country = cCountry
allocations = append(allocations, unsafe.Pointer(cCountry))
}
cleanup := func() {
for _, ptr := range allocations {
C.free(ptr)
}
}
return cOpts, cleanup
}
func GetDefaultAddressParserOptions() AddressParserOptions {
cOpts := C.libpostal_get_address_parser_default_options()
return AddressParserOptions{
Language: C.GoString(cOpts.language),
Country: C.GoString(cOpts.country),
}
}
func parseAddress(addressToParse string, options AddressParserOptions) (Address, error) {
address_cstr := C.CString(addressToParse)
defer C.free(unsafe.Pointer(address_cstr))
cOpts, cleanup := options.toC()
defer cleanup()
var address Address
response := C.libpostal_parse_address(address_cstr, cOpts)
defer C.libpostal_address_parser_response_destroy(response)
labelsSlice := unsafe.Slice(response.labels, int(response.num_components))
componentSlice := unsafe.Slice(response.components, int(response.num_components))
for i := range int(response.num_components) {
label := C.GoString(labelsSlice[i])
component := C.GoString(componentSlice[i])
switch label {
case "house_number":
address.HouseNumber = strings.Clone(component)
case "road":
address.Road = strings.Clone(component)
case "city":
address.City = strings.Clone(component)
case "state":
address.State = strings.Clone(component)
case "postcode":
address.PostCode = strings.Clone(component)
default:
return Address{}, fmt.Errorf("unrecognized label: %s (%s)", label, component)
}
}
// is the Address valid?
if err := address.Invalid(); err != nil {
return Address{}, err
}
return address, nil
}

36
parser.go Normal file
View File

@ -0,0 +1,36 @@
package libpostal
import (
"errors"
"sync"
)
var (
ErrSettingUp = errors.New("error setting up libpostal")
)
type Parser struct {
serializer sync.Mutex
once sync.Once
}
func NewParser() (*Parser, error) {
if !setup() || !setupParser() {
return nil, ErrSettingUp
}
return new(Parser), nil
}
func (p *Parser) Parse(addressToParse string, options AddressParserOptions) (Address, error) {
p.serializer.Lock()
defer p.serializer.Unlock()
return parseAddress(addressToParse, options)
}
func (p *Parser) Close() error {
p.once.Do(func() {
tearDownParser()
tearDown()
})
return nil
}

84
parser_test.go Normal file
View File

@ -0,0 +1,84 @@
package libpostal
import (
"math/rand/v2"
"os"
"testing"
)
var TheParser *Parser
func TestMain(m *testing.M) {
exitCode := func() int {
parser, err := NewParser()
if err != nil {
panic("error building new parser: " + err.Error())
}
defer parser.Close()
TheParser = parser
return m.Run()
}()
os.Exit(exitCode)
}
type TestAddress struct {
AddressToParse string
Address Address
}
var TheTestAddresses = []TestAddress{
{"1 S Huron St. Ypsilanti, MI 48197", Address{"1", "s huron st.", "ypsilanti", "mi", "48197"}},
{"1600 Pennsylvania Avenue NW Washington DC 20500", Address{"1600", "pennsylvania avenue nw", "washington", "dc", "20500"}},
{"12633 2/9 Memorial Dr, Houston, TX 77024", Address{"12633 2/9", "memorial dr", "houston", "tx", "77024"}},
}
func TestParsing(t *testing.T) {
options := GetDefaultAddressParserOptions()
for _, testAddress := range TheTestAddresses {
parsed, err := TheParser.Parse(testAddress.AddressToParse, options)
if err != nil {
t.Fatalf("Error: %v\n", err)
} else if want, got := testAddress.Address, parsed; want != got {
t.Fatalf("Error: wanted %+v; got %+v\n", want, got)
}
}
}
func TestParseEmptyString(t *testing.T) {
addressToParse := ""
options := GetDefaultAddressParserOptions()
parsed, err := TheParser.Parse(addressToParse, options)
if err == nil {
t.Fatalf("error: empty address should be invalid\n")
} else if want, got := (Address{}), parsed; want != got {
t.Fatalf("error: wanted %+v; got %+v\n", want, got)
}
}
func BenchmarkSetup(b *testing.B) {
for b.Loop() {
if !setup() {
panic("error setting up\n")
} else if !setupParser() {
panic("error setting up parser\n")
}
}
}
func BenchmarkParse(b *testing.B) {
randomAddress := func() TestAddress {
return TheTestAddresses[rand.IntN(len(TheTestAddresses))]
}
options := GetDefaultAddressParserOptions()
for b.Loop() {
TheParser.Parse(randomAddress().AddressToParse, options)
}
}
func BenchmarkTearDown(b *testing.B) {
for b.Loop() {
tearDownParser()
tearDown()
}
}