Store addresses on every geocode
This commit is contained in:
parent
e04b86218d
commit
730f40956f
12 changed files with 223 additions and 229 deletions
125
platform/geocode/address.go
Normal file
125
platform/geocode/address.go
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
package geocode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Gleipnir-Technology/bob"
|
||||
"github.com/Gleipnir-Technology/bob/dialect/psql"
|
||||
"github.com/Gleipnir-Technology/bob/dialect/psql/im"
|
||||
//bobtypes "github.com/Gleipnir-Technology/bob/types"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/h3utils"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/stadia"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/stephenafamo/scan"
|
||||
)
|
||||
|
||||
// Ensure the provided address exists. If it doesn't add it to the database.
|
||||
func EnsureAddress(ctx context.Context, txn bob.Executor, a types.Address, l types.Location) (*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
|
||||
}
|
||||
id, err := insertAddress(ctx, txn, a, l)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("insert address: %w", err)
|
||||
}
|
||||
return &models.Address{
|
||||
Country: a.Country,
|
||||
Created: time.Now(),
|
||||
Gid: a.GID,
|
||||
H3cell: "",
|
||||
ID: *id,
|
||||
Locality: a.Locality,
|
||||
Location: "",
|
||||
PostalCode: a.PostalCode,
|
||||
Street: a.Street,
|
||||
Unit: a.Unit,
|
||||
Region: a.Region,
|
||||
Number: a.Number,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ensureAddressFromFeature(ctx context.Context, txn bob.Executor, feature stadia.GeocodeFeature) (int32, error) {
|
||||
if feature.Geometry.Type != "Point" {
|
||||
return 0, fmt.Errorf("Can't hanlde stadia geometry %s", feature.Geometry.Type)
|
||||
}
|
||||
lat := feature.Geometry.Coordinates[1]
|
||||
lng := feature.Geometry.Coordinates[0]
|
||||
cell, err := h3utils.GetCell(lng, lat, 15)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to convert lat %f lng %f to h3 cell", lat, lng)
|
||||
}
|
||||
type _row struct {
|
||||
ID int32 `db:"id"`
|
||||
}
|
||||
row, err := bob.One(ctx, txn, psql.Insert(
|
||||
im.Into("address", "country", "created", "gid", "h3cell", "id", "locality", "location", "number_", "postal_code", "region", "street", "unit"),
|
||||
im.Values(
|
||||
psql.Arg(feature.CountryCode()),
|
||||
psql.Arg(time.Now()),
|
||||
psql.Arg(feature.Properties.GID),
|
||||
psql.Arg(cell.String()),
|
||||
psql.Raw("DEFAULT"),
|
||||
psql.Arg(feature.Locality()),
|
||||
psql.F("ST_Point", lng, lat, 4326),
|
||||
psql.Arg(feature.Number()),
|
||||
psql.Arg(feature.PostalCode()),
|
||||
psql.Arg(feature.Region()),
|
||||
psql.Arg(feature.Street()),
|
||||
psql.Raw("''"),
|
||||
),
|
||||
im.Returning("id"),
|
||||
), scan.StructMapper[_row]())
|
||||
log.Info().Int32("id", row.ID).Msg("inserted address")
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("insert: %w", err)
|
||||
}
|
||||
return row.ID, nil
|
||||
}
|
||||
func insertAddress(ctx context.Context, txn bob.Executor, address types.Address, location types.Location) (*int32, error) {
|
||||
cell, err := h3utils.GetCell(location.Longitude, location.Latitude, 15)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert lat %f lng %f to h3 cell", location.Longitude, location.Latitude)
|
||||
}
|
||||
type _row struct {
|
||||
ID int32 `db:"id"`
|
||||
}
|
||||
row, err := bob.One(ctx, txn, psql.Insert(
|
||||
im.Into("address", "country", "created", "gid", "h3cell", "id", "locality", "location", "number_", "postal_code", "region", "street", "unit"),
|
||||
im.Values(
|
||||
psql.Arg(address.Country),
|
||||
psql.Arg(time.Now()),
|
||||
psql.Arg(address.GID),
|
||||
psql.Arg(cell),
|
||||
psql.Raw("DEFAULT"),
|
||||
psql.Arg(address.Locality),
|
||||
psql.F("ST_Point", location.Longitude, location.Latitude, 4326),
|
||||
psql.Arg(address.Number),
|
||||
psql.Arg(address.PostalCode),
|
||||
psql.Arg(address.Region),
|
||||
psql.Arg(address.Street),
|
||||
psql.Raw("''"),
|
||||
),
|
||||
im.Returning("id"),
|
||||
), scan.StructMapper[_row]())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("insert: %w", err)
|
||||
}
|
||||
|
||||
return &row.ID, nil
|
||||
}
|
||||
func insertAddresses(ctx context.Context, txn bob.Executor, features []stadia.GeocodeFeature) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/h3utils"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/stadia"
|
||||
|
|
@ -29,10 +30,15 @@ func ByGID(ctx context.Context, gid string) (*GeocodeResult, error) {
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("latlngtocell: %w", err)
|
||||
}
|
||||
id, err := ensureAddressFromFeature(ctx, db.PGInstance.BobDB, feature)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("insert address: %w", err)
|
||||
}
|
||||
return &GeocodeResult{
|
||||
Address: types.Address{
|
||||
Country: feature.Properties.Context.ISO3166A3,
|
||||
GID: feature.Properties.GID,
|
||||
ID: &id,
|
||||
Locality: feature.Properties.Context.WhosOnFirst.Locality.Name,
|
||||
Number: feature.Properties.AddressComponents.Number,
|
||||
PostalCode: feature.Properties.AddressComponents.PostalCode,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import (
|
|||
|
||||
"github.com/Gleipnir-Technology/bob"
|
||||
"github.com/Gleipnir-Technology/bob/dialect/psql"
|
||||
"github.com/Gleipnir-Technology/bob/dialect/psql/im"
|
||||
"github.com/Gleipnir-Technology/bob/dialect/psql/sm"
|
||||
bobtypes "github.com/Gleipnir-Technology/bob/types"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/db"
|
||||
|
|
@ -18,7 +17,6 @@ import (
|
|||
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/stadia"
|
||||
"github.com/aarondl/opt/omit"
|
||||
"github.com/stephenafamo/scan"
|
||||
//"github.com/rs/zerolog/log"
|
||||
"github.com/uber/h3-go/v4"
|
||||
"resty.dev/v3"
|
||||
|
|
@ -52,127 +50,6 @@ func restyMiddleware(rclient *resty.Client, response *resty.Response) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Ensure the provided address exists. If it doesn't add it to the database.
|
||||
func EnsureAddress(ctx context.Context, txn bob.Executor, a types.Address, l types.Location) (*models.Address, error) {
|
||||
address, err := models.Addresses.Query(
|
||||
models.SelectWhere.Addresses.Country.EQ(a.CountryEnum()),
|
||||
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
|
||||
}
|
||||
cell, err := h3utils.GetCell(l.Longitude, l.Latitude, 15)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert lat %f lng %f to h3 cell", l.Longitude, l.Latitude)
|
||||
}
|
||||
type _row struct {
|
||||
ID int32 `db:"id"`
|
||||
}
|
||||
created := time.Now()
|
||||
row, err := bob.One(ctx, txn, psql.Insert(
|
||||
im.Into("address", "country", "created", "gid", "h3cell", "id", "locality", "location", "number_", "postal_code", "region", "street", "unit"),
|
||||
im.Values(
|
||||
psql.Arg(a.CountryEnum()),
|
||||
psql.Arg(created),
|
||||
psql.Arg(a.GID),
|
||||
psql.Arg(cell),
|
||||
psql.Raw("DEFAULT"),
|
||||
psql.Arg(a.Locality),
|
||||
psql.F("ST_Point", l.Longitude, l.Latitude, 4326),
|
||||
psql.Arg(a.Number),
|
||||
psql.Arg(a.PostalCode),
|
||||
psql.Arg(a.Region),
|
||||
psql.Arg(a.Street),
|
||||
psql.Raw("''"),
|
||||
),
|
||||
im.Returning("id"),
|
||||
), scan.StructMapper[_row]())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("insert: %w", err)
|
||||
}
|
||||
return &models.Address{
|
||||
Country: a.CountryEnum(),
|
||||
Created: created,
|
||||
Gid: a.GID,
|
||||
H3cell: "",
|
||||
ID: row.ID,
|
||||
Locality: a.Locality,
|
||||
Location: "",
|
||||
PostalCode: a.PostalCode,
|
||||
Street: a.Street,
|
||||
Unit: a.Unit,
|
||||
Region: a.Region,
|
||||
Number: a.Number,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 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 EnsureAddressWithGeocode(ctx context.Context, txn bob.Executor, org *models.Organization, a types.Address) (*models.Address, error) {
|
||||
address, err := models.Addresses.Query(
|
||||
models.SelectWhere.Addresses.Country.EQ(a.CountryEnum()),
|
||||
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 := GeocodeStructured(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", "gid", "h3cell", "id", "locality", "location", "number_", "postal_code", "region", "street", "unit"),
|
||||
im.Values(
|
||||
psql.Arg(geo.Address.Country),
|
||||
psql.Arg(created),
|
||||
psql.Arg(geo.Address.GID),
|
||||
psql.Arg(geo.Cell),
|
||||
psql.Raw("DEFAULT"),
|
||||
psql.Arg(geo.Address.Locality),
|
||||
psql.F("ST_Point", geo.Location.Longitude, geo.Location.Latitude, 4326),
|
||||
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.CountryEnum(),
|
||||
Created: created,
|
||||
Gid: geo.Address.GID,
|
||||
H3cell: "",
|
||||
ID: row.ID,
|
||||
Locality: geo.Address.Locality,
|
||||
Location: "",
|
||||
PostalCode: geo.Address.PostalCode,
|
||||
Street: geo.Address.Street,
|
||||
Unit: geo.Address.Unit,
|
||||
Region: geo.Address.Region,
|
||||
Number: geo.Address.Number,
|
||||
}, nil
|
||||
}
|
||||
func GeocodeRaw(ctx context.Context, org *models.Organization, address string) (*GeocodeResult, error) {
|
||||
req := stadia.RequestGeocodeRaw{
|
||||
Text: address,
|
||||
|
|
@ -182,6 +59,7 @@ func GeocodeRaw(ctx context.Context, org *models.Organization, address string) (
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("client raw geocode failure on %s: %w", address, err)
|
||||
}
|
||||
insertAddresses(ctx, db.PGInstance.BobDB, resp.Features)
|
||||
return toGeocodeResult(*resp, address)
|
||||
}
|
||||
func GeocodeStructured(ctx context.Context, org *models.Organization, a types.Address) (*GeocodeResult, error) {
|
||||
|
|
@ -198,6 +76,7 @@ func GeocodeStructured(ctx context.Context, org *models.Organization, a types.Ad
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("client structured geocode failure on %s: %w", a.String(), err)
|
||||
}
|
||||
insertAddresses(ctx, db.PGInstance.BobDB, resp.Features)
|
||||
return toGeocodeResult(*resp, a.String())
|
||||
}
|
||||
func ReverseGeocode(ctx context.Context, location types.Location) (*GeocodeResult, error) {
|
||||
|
|
@ -209,6 +88,7 @@ func ReverseGeocode(ctx context.Context, location types.Location) (*GeocodeResul
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("client reverse geocode failure on %s: %w", location.String(), err)
|
||||
}
|
||||
insertAddresses(ctx, db.PGInstance.BobDB, resp.Features)
|
||||
return toGeocodeResult(*resp, location.String())
|
||||
|
||||
}
|
||||
|
|
@ -264,10 +144,13 @@ func toGeocodeResult(resp stadia.GeocodeResponse, address_msg string) (*GeocodeR
|
|||
}
|
||||
|
||||
// Get the parcel for a given address, if one can be found
|
||||
func GetParcel(ctx context.Context, txn bob.Executor, a *models.Address) (*models.Parcel, error) {
|
||||
func GetParcel(ctx context.Context, txn bob.Executor, a types.Address) (*models.Parcel, error) {
|
||||
if a.ID == nil {
|
||||
return nil, fmt.Errorf("nil address ID")
|
||||
}
|
||||
result, err := models.Parcels.Query(
|
||||
sm.InnerJoin("address").On(psql.F("ST_Contains", psql.Raw("parcel.geometry"), psql.Raw("address.location"))),
|
||||
models.SelectWhere.Addresses.ID.EQ(a.ID),
|
||||
models.SelectWhere.Addresses.ID.EQ(*a.ID),
|
||||
).One(ctx, txn)
|
||||
if err != nil {
|
||||
if err.Error() == "sql: no rows in result set" {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue