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