first commit
This commit is contained in:
commit
d492948756
5
README.md
Normal file
5
README.md
Normal 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
38
address.go
Normal 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
|
||||
}
|
||||
135
libpostal.go
Normal file
135
libpostal.go
Normal 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
36
parser.go
Normal 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
84
parser_test.go
Normal 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()
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user