From d492948756197345e75511f9c79c1ae09c848fb2 Mon Sep 17 00:00:00 2001 From: William Dillon Date: Sat, 8 Aug 2026 08:50:11 -0400 Subject: [PATCH] first commit --- README.md | 5 ++ address.go | 38 ++++++++++++++ go.mod | 3 ++ libpostal.go | 135 +++++++++++++++++++++++++++++++++++++++++++++++++ parser.go | 36 +++++++++++++ parser_test.go | 84 ++++++++++++++++++++++++++++++ 6 files changed, 301 insertions(+) create mode 100644 README.md create mode 100644 address.go create mode 100644 go.mod create mode 100644 libpostal.go create mode 100644 parser.go create mode 100644 parser_test.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..b2cac4b --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# libpostal +libpostal is a cgo wrapper over the libpostal lib for parsing postal addresses. + +## requirements +libpostal shared library \ No newline at end of file diff --git a/address.go b/address.go new file mode 100644 index 0000000..707f39e --- /dev/null +++ b/address.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7c91829 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module code.wmdillon.com/wmdillon/libpostal + +go 1.25 diff --git a/libpostal.go b/libpostal.go new file mode 100644 index 0000000..2694fdd --- /dev/null +++ b/libpostal.go @@ -0,0 +1,135 @@ +/* +The functions in this file are unexported to ensure safe use through the Parser. +*/ + +package libpostal + +//#cgo LDFLAGS: -lpostal +//#include +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 +} diff --git a/parser.go b/parser.go new file mode 100644 index 0000000..9f257f9 --- /dev/null +++ b/parser.go @@ -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 +} diff --git a/parser_test.go b/parser_test.go new file mode 100644 index 0000000..2256573 --- /dev/null +++ b/parser_test.go @@ -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() + } +}