From b29d1720302521f7bd8d1503c68b103e22aaf32e Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Sat, 14 Mar 2026 01:49:59 +0000 Subject: [PATCH] Add better support for extracting address from reverse geocode results Stadia's API sucks. They don't really tell you what their response will be in detail, just claim they are all the same, but they're not. Not even a little. --- .gitignore | 1 + platform/geocode/geocode.go | 39 ++++++--- stadia/cmd/reverse-geocode/main.go | 49 +++++++++++ stadia/response_type.go | 131 +++++++++++++++++++++++------ 4 files changed, 180 insertions(+), 40 deletions(-) create mode 100644 stadia/cmd/reverse-geocode/main.go diff --git a/.gitignore b/.gitignore index 4fe8cbc7..a20e0bd3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,6 @@ nidus-sync nidus-sync.log result stadia/cmd/bulk-geocode/bulk-geocode +stadia/cmd/reverse-geocode/reverse-geocode stadia/cmd/structured-geocode/structured-geocode tmp/ diff --git a/platform/geocode/geocode.go b/platform/geocode/geocode.go index 943faaec..33da4623 100644 --- a/platform/geocode/geocode.go +++ b/platform/geocode/geocode.go @@ -187,18 +187,18 @@ func ReverseGeocode(ctx context.Context, location types.Location) (*GeocodeResul return toGeocodeResult(*resp, location.String()) } -func toGeocodeResult(resp stadia.GeocodeResponse, address string) (*GeocodeResult, error) { +func toGeocodeResult(resp stadia.GeocodeResponse, address_msg string) (*GeocodeResult, error) { if len(resp.Features) < 1 { - return nil, fmt.Errorf("%s matched no locations", address) + return nil, fmt.Errorf("%s matched no locations", address_msg) } feature := resp.Features[0] if len(resp.Features) > 1 { if !allFeaturesIdenticalEnough(resp.Features) { - return nil, fmt.Errorf("%s matched more than one location, and they differ a lot", address) + return nil, fmt.Errorf("%s matched more than one location, and they differ a lot", address_msg) } } if feature.Geometry.Type != "Point" { - return nil, fmt.Errorf("wrong type %s from %s", feature.Geometry.Type, address) + return nil, fmt.Errorf("wrong type %s from %s", feature.Geometry.Type, address_msg) } longitude := feature.Geometry.Coordinates[0] latitude := feature.Geometry.Coordinates[1] @@ -207,17 +207,28 @@ func toGeocodeResult(resp stadia.GeocodeResponse, address string) (*GeocodeResul return nil, fmt.Errorf("failed to convert lat %f lng %f to h3 cell", longitude, latitude) } country_s := strings.ToLower(feature.Properties.CountryA) + // Depending on what kind of request we made we'll get wildly different result structures + // This first structure generally works for forword geocoding + address := types.Address{ + Country: country_s, + Locality: feature.Properties.Locality, + Number: feature.Properties.HouseNumber, + PostalCode: feature.Properties.PostalCode, + Region: feature.Properties.Region, + Street: feature.Properties.Street, + Unit: "", + } + // If we don't have a locality, try populating for reverse geocoding + if address.Country == "" { + address.Country = strings.ToLower(feature.Properties.Context.ISO3166A3) + address.Locality = feature.Properties.Context.WhosOnFirst.Locality.Name + address.Number = feature.Properties.AddressComponents.Number + address.PostalCode = feature.Properties.AddressComponents.PostalCode + address.Street = feature.Properties.AddressComponents.Street + } return &GeocodeResult{ - Address: types.Address{ - Country: country_s, - Locality: feature.Properties.Locality, - Number: feature.Properties.HouseNumber, - PostalCode: feature.Properties.PostalCode, - Region: feature.Properties.Region, - Street: feature.Properties.Street, - Unit: "", - }, - Cell: cell, + Address: address, + Cell: cell, Location: types.Location{ Longitude: feature.Geometry.Coordinates[0], Latitude: feature.Geometry.Coordinates[1], diff --git a/stadia/cmd/reverse-geocode/main.go b/stadia/cmd/reverse-geocode/main.go new file mode 100644 index 00000000..112c34e6 --- /dev/null +++ b/stadia/cmd/reverse-geocode/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "context" + "flag" + "log" + "os" + + "github.com/Gleipnir-Technology/nidus-sync/stadia" +) + +func main() { + // Define command-line flags + lat := flag.Float64("lat", 0, "The latitude of the point") + lng := flag.Float64("lng", 0, "The longitude of the point") + + // Parse the flags + flag.Parse() + + if *lat == 0 || *lng == 0 { + log.Println("Error: you must specify both lat and lng") + flag.Usage() + os.Exit(1) + } + + key := os.Getenv("STADIA_MAPS_API_KEY") + if key == "" { + log.Println("STADIA_MAPS_API_KEY is empty") + os.Exit(1) + } + + client := stadia.NewStadiaMaps(key) + ctx := context.Background() + req := stadia.RequestReverseGeocode{ + Latitude: *lat, + Longitude: *lng, + } + resp, err := client.ReverseGeocode(ctx, req) + if err != nil { + log.Printf("err: %v\n", err) + os.Exit(2) + } + log.Printf("type: %s, features: %d\n", resp.Type, len(resp.Features)) + for i, feature := range resp.Features { + log.Printf("feature %d: type %s\n", i, feature.Type) + log.Printf("\tgeometry %s (%f %f)\n", feature.Geometry.Type, feature.Geometry.Coordinates[0], feature.Geometry.Coordinates[1]) + log.Printf("\tproperties %s\n", feature.Properties.Layer) + } +} diff --git a/stadia/response_type.go b/stadia/response_type.go index c45a46b4..76b1f1b5 100644 --- a/stadia/response_type.go +++ b/stadia/response_type.go @@ -1,5 +1,82 @@ package stadia +/* + "address_components": { + "number": "3397", + "postal_code": "84065", + "street": "West Chatel Drive" + }, +*/ +type AddressComponents struct { + Number string `json:"number"` + PostalCode string `json:"postal_code"` + Street string `json:"street"` +} +type Country struct { + Abbreviation string `json:"abbreviation"` + GID string `json:"gid"` + Name string `json:"name"` +} +type County struct { + Abbreviation string `json:"abbreviation"` + GID string `json:"gid"` + Name string `json:"name"` +} +type Locality struct { + GID string `json:"gid"` + Name string `json:"name"` +} +type Region struct { + Abbreviation string `json:"abbreviation"` + GID string `json:"gid"` + Name string `json:"name"` +} + +/* + "country": { + "abbreviation": "USA", + "gid": "whosonfirst:country:85633793", + "name": "United States" + }, + + "county": { + "abbreviation": "SL", + "gid": "whosonfirst:county:102082877", + "name": "Salt Lake County" + }, + + "locality": { + "gid": "whosonfirst:locality:101728073", + "name": "Riverton" + }, + + "region": { + "abbreviation": "UT", + "gid": "whosonfirst:region:85688567", + "name": "Utah" + } +*/ +type ContextWhosOnFirst struct { + Country Country `json:"country"` + County County `json:"county"` + Locality Locality `json:"locality"` + Region Region `json:"region"` +} + +/* + "context": { + "iso_3166_a2": "US", + "iso_3166_a3": "USA", + "whosonfirst": {...} + } + } +*/ +type Context struct { + ISO3166A2 string `json:"iso_3166_a2"` + ISO3166A3 string `json:"iso_3166_a3"` + WhosOnFirst ContextWhosOnFirst `json:"whosonfirst,omitempty"` +} + // GeocodeResponse represents the top-level response from the geocoding API type GeocodeResponse struct { BBox []float64 `json:"bbox"` // [W, S, E, N] @@ -33,32 +110,34 @@ type GeocodeGeometry struct { // GeocodeProperties contains the properties of a geocoding result type GeocodeProperties struct { - Addendum map[string]interface{} `json:"addendum,omitempty"` - Accuracy string `json:"accuracy"` // 'point' - Confidence float64 `json:"confidence"` // 1 - Country string `json:"country"` // 'United States' - CountryA string `json:"country_a"` // 'USA' - CountryCode string `json:"country_code"` // 'US' - CountryGID string `json:"country_gid"` // 'whosonfirst:country:85633793' - County string `json:"county"` // "Tulare County" - CountyA string `json:"county_a"` // 'TL' - CountyGID string `json:"county_gid"` // 'whosonfirst:county:102082895' - GID string `json:"gid"` // 'openaddresses:address:us/ca/tulare-addresses-county:fe9dfab3d45c4550' - HouseNumber string `json:"housenumber"` // '1234' - ID string `json:"id"` // us/ca/tulare-addresses-county:fe9dfab3d45c4550 - Label string `json:"label"` // 1234 Main St, Dinuba, CA, USA - Layer string `json:"layer"` // 'address' - Locality string `json:"locality"` // 'Dinuba' - LocalityGID string `json:"locality_gid"` // 'whosonfirst:locality:85922491' - MatchType string `json:"match_type"` // 'exact' - Name string `json:"name"` // '1234 Main St' - PostalCode string `json:"postalcode"` // '93618' - Region string `json:"region"` // 'California' - RegionA string `json:"region_a"` // 'CA' - RegionGID string `json:"region_gid"` // 'whosonfirst:region:85688637' - Source string `json:"source"` // 'openaddresses' - SourceID string `json:"source"` // 'us/ca/tulare-addresses-county:fe9dfab3d45c4550' - Street string `json:"street"` // 'Main Street' + Addendum map[string]interface{} `json:"addendum,omitempty"` + AddressComponents AddressComponents `json:"address_components,omitempty"` + Accuracy string `json:"accuracy"` // 'point' + Confidence float64 `json:"confidence"` // 1 + Context Context `json:"context,omitempty"` // bunch of stuff + Country string `json:"country"` // 'United States' + CountryA string `json:"country_a"` // 'USA' + CountryCode string `json:"country_code"` // 'US' + CountryGID string `json:"country_gid"` // 'whosonfirst:country:85633793' + County string `json:"county"` // "Tulare County" + CountyA string `json:"county_a"` // 'TL' + CountyGID string `json:"county_gid"` // 'whosonfirst:county:102082895' + GID string `json:"gid"` // 'openaddresses:address:us/ca/tulare-addresses-county:fe9dfab3d45c4550' + HouseNumber string `json:"housenumber"` // '1234' + ID string `json:"id"` // us/ca/tulare-addresses-county:fe9dfab3d45c4550 + Label string `json:"label"` // 1234 Main St, Dinuba, CA, USA + Layer string `json:"layer"` // 'address' + Locality string `json:"locality"` // 'Dinuba' + LocalityGID string `json:"locality_gid"` // 'whosonfirst:locality:85922491' + MatchType string `json:"match_type"` // 'exact' + Name string `json:"name"` // '1234 Main St' + PostalCode string `json:"postalcode"` // '93618' + Region string `json:"region"` // 'California' + RegionA string `json:"region_a"` // 'CA' + RegionGID string `json:"region_gid"` // 'whosonfirst:region:85688637' + Source string `json:"source"` // 'openaddresses' + SourceID string `json:"source"` // 'us/ca/tulare-addresses-county:fe9dfab3d45c4550' + Street string `json:"street"` // 'Main Street' } // GeocodeSource represents a source of geocoding data