Skip to content

Commit 4ae1f9b

Browse files
committed
Libpostal rest server implementation
1 parent 72530c3 commit 4ae1f9b

File tree

12 files changed

+1156
-1
lines changed

12 files changed

+1156
-1
lines changed

.gitignore

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# If you prefer the allow list template instead of the deny list, see community template:
2+
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3+
#
4+
# Binaries for programs and plugins
5+
*.exe
6+
*.exe~
7+
*.dll
8+
*.so
9+
*.dylib
10+
11+
# Test binary, built with `go test -c`
12+
*.test
13+
14+
# Output of the go coverage tool, specifically when used with LiteIDE
15+
*.out
16+
17+
# Dependency directories (remove the comment below to include it)
18+
# vendor/
19+
20+
# Go workspace file
21+
go.work
22+
.env

Dockerfile

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
FROM golang:alpine3.17
2+
3+
RUN set -ex \
4+
&& apk add --no-cache --virtual .build-deps \
5+
curl \
6+
gcc \
7+
g++ \
8+
make \
9+
libtool \
10+
autoconf \
11+
automake \
12+
git \
13+
&& mkdir -p /src \
14+
&& mkdir -p /data \
15+
&& cd /src \
16+
&& git clone https://github.com/openvenues/libpostal.git \
17+
&& cd libpostal \
18+
&& ./bootstrap.sh \
19+
&& ./configure --datadir=/data MODEL=senzing \
20+
&& make -j "$(nproc)" \
21+
&& make install \
22+
&& apk del .build-deps \
23+
&& rm -rf /src
24+
25+
RUN apk add --no-cache gcc musl-dev pkgconfig
26+
27+
WORKDIR /app
28+
29+
ENV GO111MODULE=on
30+
ENV CGO_ENABLED=1
31+
ENV GOOS=linux
32+
ENV GOARCH=amd64
33+
34+
COPY go.mod go.sum ./
35+
RUN go mod download
36+
37+
COPY . .
38+
39+
RUN go build -o /usr/bin/address-parser main.go
40+
41+
ENTRYPOINT ["/usr/bin/address-parser"]

README.md

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,77 @@
1-
# address-parser-go-rest
1+
# Address Parser Go REST
2+
3+
Address Parser Go REST is a REST API that provides address parsing functionality using the libpostal library.
4+
The purpose of this API is to allow users to easily parse addresses into their individual components
5+
without the need for the libpostal library to be included as a dependency in their projects.
6+
7+
## Quickstart
8+
9+
```
10+
docker run
11+
12+
curl -X 'POST' \
13+
'http://localhost:8080/parse' \
14+
-H 'accept: application/json' \
15+
-H 'Content-Type: application/json' \
16+
-d '{
17+
"address": "48 Leicester Square, London WC2H 7LU, United Kingdom",
18+
"title_case": true
19+
}'
20+
```
21+
22+
Response:
23+
24+
```
25+
curl -X 'POST' \
26+
'http://localhost:8080/parse' \
27+
-H 'accept: application/json' \
28+
-H 'Content-Type: application/json' \
29+
-d '{
30+
"address": "48 Leicester Square, London WC2H 7LU, United Kingdom",
31+
"title_case": true
32+
}'
33+
```
34+
35+
[swagger documentation](http://localhost:8080/docs/)
36+
37+
38+
## Run without docker
39+
40+
To install and run Address Parser Go REST, you can use the following steps:
41+
42+
1. Make sure you have a recent version of Golang
43+
2. [Install](https://github.com/openvenues/libpostal/issues#installation-maclinux) libpostal on your machine.
44+
3. `go mod tidy`
45+
4. `go run main.go`
46+
47+
48+
Notes:
49+
you can change the port the service or the path for swagger is listening to by setting the following environment variables:
50+
```
51+
PARSER_HTTP_ADDR=:8080
52+
DOCS_PATH=/docs
53+
```
54+
you can also put these in `.env` file in the root of the project.
55+
56+
If you want to rebuild the swagger documentation make sure that you have
57+
installed [swag](https://github.com/swaggo/swag)
58+
59+
to regenerate:
60+
```
61+
go generate
62+
```
63+
64+
## Contributing
65+
66+
If you would like to contribute to Address Parser Go REST, please create a pull request with your changes.
67+
You can also report any issues or bugs you encounter by creating a new issue on the GitHub repository.
68+
69+
## License
70+
71+
Address Parser Go REST is licensed under the MIT License. See `LICENSE` for more information.
72+
73+
## Acknowledgments
74+
75+
We would like to acknowledge the contributors of the libpostal library and the Go bindings used in this project.
76+
77+

addressparser/addressparser.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package addressparser
2+
3+
import "errors"
4+
5+
var ErrAddressUnparsable = errors.New("address is unparsable")
6+
7+
// Address is a struct for an address
8+
type Address struct {
9+
// venue name e.g. "Brooklyn Academy of Music", and building names e.g. "Empire State Building"
10+
House string `json:"house,omitempty"`
11+
// for category queries like "restaurants", etc.
12+
Category string `json:"category,omitempty"`
13+
// phrases like "in", "near", etc. used after a category phrase to help with parsing queries like "restaurants in Brooklyn"
14+
Near string `json:"near,omitempty"`
15+
// usually refers to the external (street-facing) building number. In some countries this may be a compount, hyphenated number which also includes an apartment number, or a block number (a la Japan), but libpostal will just call it the house_number for simplicity.
16+
HouseNumber string `json:"house_number,omitempty"`
17+
// street name(s)
18+
Road string `json:"road,omitempty"`
19+
// an apartment, unit, office, lot, or other secondary unit designator
20+
Unit string `json:"unit,omitempty"`
21+
// expressions indicating a floor number e.g. "3rd Floor", "Ground Floor", etc.
22+
Level string `json:"level,omitempty"`
23+
// numbered/lettered staircase
24+
Staircase string `json:"staircase,omitempty"`
25+
// numbered/lettered entrance
26+
Entrance string `json:"entrance,omitempty"`
27+
// post office box: typically found in non-physical (mail-only) addresses
28+
PoBox string `json:"po_box,omitempty"`
29+
// postal codes used for mail sorting
30+
Postcode string `json:"postcode,omitempty"`
31+
// usually an unofficial neighborhood name like "Harlem", "South Bronx", or "Crown Heights"
32+
Suburb string `json:"suburb,omitempty"`
33+
// these are usually boroughs or districts within a city that serve some official purpose e.g. "Brooklyn" or "Hackney" or "Bratislava IV"
34+
CityDistrict string `json:"city_district,omitempty"`
35+
// any human settlement including cities, towns, villages, hamlets, localities, etc.
36+
City string `json:"city,omitempty"`
37+
// named islands e.g. "Maui"
38+
Island string `json:"island,omitempty"`
39+
// usually a second-level administrative division or county.
40+
StateDistrict string `json:"state_district,omitempty"`
41+
// a first-level administrative division. Scotland, Northern Ireland, Wales, and England in the UK are mapped to "state" as well (convention used in OSM, GeoPlanet, etc.)
42+
State string `json:"state,omitempty"`
43+
// informal subdivision of a country without any political status
44+
CountryRegion string `json:"country_region,omitempty"`
45+
// sovereign nations and their dependent territories, anything with an ISO-3166 code.
46+
Country string `json:"country,omitempty"`
47+
// currently only used for appending “West Indies” after the country name, a pattern frequently used in the English-speaking Caribbean e.g. “Jamaica, West Indies”
48+
WorldRegion string `json:"world_region,omitempty"`
49+
}
50+
51+
// AddressParserInput is a struct for the input to the address parser
52+
type AddressParserInput struct {
53+
// the address to parse
54+
Address string `json:"address" validate:"required"`
55+
// the language of the address. Leave empty if you don't know
56+
Language string `json:"language,omitempty"`
57+
// the country of the address. Leave empty if you don't know
58+
Country string `json:"country,omitempty"`
59+
// if true then the responses will be title Cased. Default behavior of libpostal is not to do that.
60+
TitleCase bool `json:"title_case,omitempty"`
61+
}
62+
63+
// AddressParser is an interface for the address parser
64+
type AddressParser interface {
65+
Parse(input AddressParserInput) (Address, error)
66+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package libpostal
2+
3+
import (
4+
"github.com/gosom/kit/logging"
5+
postal "github.com/openvenues/gopostal/parser"
6+
"golang.org/x/text/cases"
7+
"golang.org/x/text/language"
8+
9+
"github.com/gosom/address-parser-go-rest/addressparser"
10+
)
11+
12+
var _ addressparser.AddressParser = (*libPostalParser)(nil)
13+
14+
type libPostalParser struct {
15+
log logging.Logger
16+
}
17+
18+
func (o *libPostalParser) Parse(input addressparser.AddressParserInput) (addressparser.Address, error) {
19+
components := postal.ParseAddressOptions(input.Address, postal.ParserOptions{
20+
Language: input.Language,
21+
Country: input.Country,
22+
})
23+
if len(components) == 0 {
24+
return addressparser.Address{}, addressparser.ErrAddressUnparsable
25+
}
26+
address := addressparser.Address{}
27+
tag := language.Und
28+
if input.Language != "" {
29+
if r, err := language.Parse("de"); err == nil {
30+
tag = r
31+
}
32+
}
33+
for i := range components {
34+
if input.TitleCase {
35+
components[i].Value = cases.Title(tag, cases.NoLower).String(components[i].Value)
36+
}
37+
switch components[i].Label {
38+
case "house":
39+
address.House = components[i].Value
40+
case "category":
41+
address.Category = components[i].Value
42+
case "near":
43+
address.Near = components[i].Value
44+
case "house_number":
45+
address.HouseNumber = components[i].Value
46+
case "road":
47+
address.Road = components[i].Value
48+
case "unit":
49+
address.Unit = components[i].Value
50+
case "level":
51+
address.Level = components[i].Value
52+
case "staircase":
53+
address.Staircase = components[i].Value
54+
case "entrance":
55+
address.Entrance = components[i].Value
56+
case "po_box":
57+
address.PoBox = components[i].Value
58+
case "postcode":
59+
address.Postcode = components[i].Value
60+
case "suburb":
61+
address.Suburb = components[i].Value
62+
case "city_district":
63+
address.CityDistrict = components[i].Value
64+
case "city":
65+
address.City = components[i].Value
66+
case "island":
67+
address.Island = components[i].Value
68+
case "state_district":
69+
address.StateDistrict = components[i].Value
70+
case "state":
71+
address.State = components[i].Value
72+
case "country_region":
73+
address.CountryRegion = components[i].Value
74+
case "country":
75+
address.Country = components[i].Value
76+
case "world_region":
77+
address.WorldRegion = components[i].Value
78+
default:
79+
o.log.Warn("Unknown component", "component", components[i].Label)
80+
}
81+
}
82+
return address, nil
83+
}
84+
85+
func NewLibPostalParser(log logging.Logger) *libPostalParser {
86+
return &libPostalParser{log: log}
87+
}

addressparser/ports/http.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package ports
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
7+
"github.com/gosom/kit/lib"
8+
"github.com/gosom/kit/logging"
9+
"github.com/gosom/kit/web"
10+
11+
"github.com/gosom/address-parser-go-rest/addressparser"
12+
)
13+
14+
// AddressParserHandler is a handler for parsing addresses
15+
type AddressParserHandler struct {
16+
log logging.Logger
17+
parser addressparser.AddressParser
18+
}
19+
20+
// NewAddressParserHandler creates a new AddressParserHandler
21+
func NewAddressParserHandler(log logging.Logger, parser addressparser.AddressParser) AddressParserHandler {
22+
return AddressParserHandler{
23+
log: log,
24+
parser: parser,
25+
}
26+
}
27+
28+
// RegisterRoutes registers the routes for the AddressParserHandler
29+
func (o *AddressParserHandler) RegisterRouters(r web.Router) {
30+
r.Post("/parse", o.Parse)
31+
}
32+
33+
// Parse is a handler for parsing addresses
34+
//
35+
// @Summary Parse an address into its components
36+
// @Description Parses an address into its components
37+
// @Tags AddressParser
38+
// @Accept json
39+
// @Produce json
40+
// @Param input body addressparser.AddressParserInput true "AddressParserInput"
41+
// @Success 200 {object} addressparser.Address
42+
// @Failure 400 {object} web.ErrResponse
43+
// @Failure 422 {object} web.ErrResponse
44+
// @Failure 500 {object} web.ErrResponse
45+
// @Router /parse [post]
46+
func (o *AddressParserHandler) Parse(w http.ResponseWriter, r *http.Request) {
47+
var payload addressparser.AddressParserInput
48+
if err := web.DecodeBody(r, &payload, true); err != nil {
49+
web.JSONError(w, r, fmt.Errorf("%w %s", lib.ErrBadRequest))
50+
return
51+
}
52+
result, err := o.parser.Parse(payload)
53+
if err != nil {
54+
ae := fmt.Errorf("%w %s", lib.ErrUnprocessable, err.Error())
55+
web.JSONError(w, r, ae)
56+
return
57+
}
58+
web.JSON(w, r, http.StatusOK, result)
59+
}

0 commit comments

Comments
 (0)