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