Push geocoding down a layer
This makes it possible to always save address information from our geocoder.
This commit is contained in:
parent
80e14568c6
commit
daa8cb1748
26 changed files with 576 additions and 431 deletions
187
platform/geocode/geocode.go
Normal file
187
platform/geocode/geocode.go
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
package geocode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Gleipnir-Technology/bob"
|
||||
"github.com/Gleipnir-Technology/bob/dialect/psql"
|
||||
"github.com/Gleipnir-Technology/bob/dialect/psql/im"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db/enums"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/h3utils"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/stadia"
|
||||
"github.com/stephenafamo/scan"
|
||||
//"github.com/rs/zerolog/log"
|
||||
"github.com/uber/h3-go/v4"
|
||||
)
|
||||
|
||||
type Address struct {
|
||||
Country enums.Countrytype
|
||||
Locality string
|
||||
Number string
|
||||
PostalCode string
|
||||
Region string
|
||||
Street string
|
||||
Unit string
|
||||
}
|
||||
type GeocodeResult struct {
|
||||
Address Address
|
||||
Cell h3.Cell
|
||||
Longitude float64
|
||||
Latitude float64
|
||||
}
|
||||
|
||||
func (a Address) String() string {
|
||||
return fmt.Sprintf("%s %s, %s, %s, %s, %s", a.Number, a.Street, a.Locality, a.Region, a.PostalCode, a.Country)
|
||||
}
|
||||
|
||||
var client *stadia.StadiaMaps
|
||||
|
||||
// Either get an address that matches, or create a new address. Either way, return an address
|
||||
// This will make a call to a structured geocode service, so it's slow.
|
||||
func EnsureAddress(ctx context.Context, txn bob.Tx, org *models.Organization, a Address) (*models.Address, error) {
|
||||
address, err := models.Addresses.Query(
|
||||
models.SelectWhere.Addresses.Country.EQ(a.Country),
|
||||
models.SelectWhere.Addresses.Locality.EQ(a.Locality),
|
||||
models.SelectWhere.Addresses.Number.EQ(a.Number),
|
||||
models.SelectWhere.Addresses.PostalCode.EQ(a.PostalCode),
|
||||
models.SelectWhere.Addresses.Region.EQ(a.Region),
|
||||
models.SelectWhere.Addresses.Street.EQ(a.Street),
|
||||
models.SelectWhere.Addresses.Unit.EQ(a.Unit),
|
||||
).One(ctx, txn)
|
||||
if err == nil {
|
||||
return address, nil
|
||||
}
|
||||
// Geocode
|
||||
geo, err := Geocode(ctx, org, a)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("geocode: %w", err)
|
||||
}
|
||||
|
||||
type _row struct {
|
||||
ID int32 `db:"id"`
|
||||
}
|
||||
created := time.Now()
|
||||
row, err := bob.One(ctx, txn, psql.Insert(
|
||||
im.Into("address", "country", "created", "geom", "h3cell", "id", "locality", "number", "postal_code", "region", "street", "unit"),
|
||||
im.Values(
|
||||
psql.Arg(geo.Address.Country),
|
||||
psql.Arg(created),
|
||||
psql.F("ST_Point", geo.Longitude, geo.Latitude, 4326),
|
||||
psql.Arg(geo.Cell),
|
||||
psql.Raw("DEFAULT"),
|
||||
psql.Arg(geo.Address.Locality),
|
||||
psql.Arg(geo.Address.Number),
|
||||
psql.Arg(geo.Address.PostalCode),
|
||||
psql.Arg(geo.Address.Region),
|
||||
psql.Arg(geo.Address.Street),
|
||||
psql.Raw("''"),
|
||||
),
|
||||
im.Returning("id"),
|
||||
), scan.StructMapper[_row]())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("insert: %w", err)
|
||||
}
|
||||
|
||||
return &models.Address{
|
||||
Country: geo.Address.Country,
|
||||
Created: created,
|
||||
Geom: "",
|
||||
H3cell: "",
|
||||
ID: row.ID,
|
||||
Locality: geo.Address.Locality,
|
||||
PostalCode: geo.Address.PostalCode,
|
||||
Street: geo.Address.Street,
|
||||
Unit: geo.Address.Unit,
|
||||
Region: geo.Address.Region,
|
||||
Number: geo.Address.Number,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func Geocode(ctx context.Context, org *models.Organization, a Address) (GeocodeResult, error) {
|
||||
street := fmt.Sprintf("%s %s", a.Number, a.Street)
|
||||
country_s := a.Country.String()
|
||||
/*
|
||||
sublog := log.With().
|
||||
Str("street", street).
|
||||
Str("country", country).
|
||||
Str("locality", a.Locality).
|
||||
Str("postal", a.PostalCode).
|
||||
Str("region", a.Region).
|
||||
Logger()
|
||||
*/
|
||||
req := stadia.StructuredGeocodeRequest{
|
||||
Address: &street,
|
||||
Country: &country_s,
|
||||
Locality: &a.Locality,
|
||||
PostalCode: &a.PostalCode,
|
||||
Region: &a.Region,
|
||||
}
|
||||
maybeAddServiceArea(&req, org)
|
||||
resp, err := client.StructuredGeocode(ctx, req)
|
||||
if err != nil {
|
||||
return GeocodeResult{}, fmt.Errorf("client structured geocode failure on %s: %w", a.String(), err)
|
||||
}
|
||||
if len(resp.Features) > 1 {
|
||||
return GeocodeResult{}, fmt.Errorf("%s matched more than one location", a.String())
|
||||
}
|
||||
feature := resp.Features[0]
|
||||
if feature.Geometry.Type != "Point" {
|
||||
return GeocodeResult{}, fmt.Errorf("wrong type %s from %s", feature.Geometry.Type, a.String())
|
||||
}
|
||||
longitude := feature.Geometry.Coordinates[0]
|
||||
latitude := feature.Geometry.Coordinates[1]
|
||||
cell, err := h3utils.GetCell(longitude, latitude, 15)
|
||||
if err != nil {
|
||||
return GeocodeResult{}, fmt.Errorf("failed to convert lat %f lng %f to h3 cell", longitude, latitude)
|
||||
}
|
||||
var country enums.Countrytype
|
||||
country_s = strings.ToLower(feature.Properties.CountryA)
|
||||
err = country.Scan(country_s)
|
||||
if err != nil {
|
||||
return GeocodeResult{}, fmt.Errorf("failed to scan country '%s': %w", country_s, err)
|
||||
}
|
||||
return GeocodeResult{
|
||||
Address: Address{
|
||||
Country: country,
|
||||
Locality: feature.Properties.Locality,
|
||||
Number: feature.Properties.HouseNumber,
|
||||
PostalCode: feature.Properties.PostalCode,
|
||||
Region: feature.Properties.Region,
|
||||
Street: feature.Properties.Street,
|
||||
Unit: "",
|
||||
},
|
||||
Cell: cell,
|
||||
Longitude: feature.Geometry.Coordinates[0],
|
||||
Latitude: feature.Geometry.Coordinates[1],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func maybeAddServiceArea(req *stadia.StructuredGeocodeRequest, org *models.Organization) {
|
||||
if org.ServiceAreaXmax.IsNull() ||
|
||||
org.ServiceAreaYmax.IsNull() ||
|
||||
org.ServiceAreaXmin.IsNull() ||
|
||||
org.ServiceAreaYmin.IsNull() {
|
||||
return
|
||||
}
|
||||
xmax := org.ServiceAreaXmax.MustGet()
|
||||
ymax := org.ServiceAreaYmax.MustGet()
|
||||
xmin := org.ServiceAreaXmin.MustGet()
|
||||
ymin := org.ServiceAreaYmin.MustGet()
|
||||
req.BoundaryRectMaxLon = &xmax
|
||||
req.BoundaryRectMaxLat = &ymax
|
||||
req.BoundaryRectMinLon = &xmin
|
||||
req.BoundaryRectMinLat = &ymin
|
||||
|
||||
if org.ServiceAreaCentroidX.IsNull() || org.ServiceAreaCentroidY.IsNull() {
|
||||
return
|
||||
}
|
||||
centroid_x := org.ServiceAreaCentroidX.MustGet()
|
||||
centroid_y := org.ServiceAreaCentroidY.MustGet()
|
||||
|
||||
req.FocusPointLat = ¢roid_y
|
||||
req.FocusPointLng = ¢roid_x
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue