├── .gitignore ├── go.mod ├── go.sum ├── LICENSE ├── chained ├── geocoder.go └── geocoder_test.go ├── data ├── geocoder.go └── geocoder_test.go ├── cached ├── geocoder.go └── geocoder_test.go ├── geocoder.go ├── osm └── osm.go ├── geocod ├── geocoder.go └── geocoder_test.go ├── mapquest ├── nominatim │ ├── geocoder.go │ └── geocoder_test.go └── open │ ├── geocoder.go │ └── geocoder_test.go ├── openstreetmap ├── geocoder.go └── geocoder_test.go ├── pickpoint ├── geocoder.go └── geocoder_test.go ├── bing ├── geocoder.go └── geocoder_test.go ├── opencage ├── geocoder.go └── geocoder_test.go ├── arcgis ├── geocoder.go └── geocoder_test.go ├── mapzen ├── geocoder.go └── geocoder_test.go ├── locationiq ├── geocoder.go └── geocoder_test.go ├── here ├── search │ ├── geocoder.go │ └── geocoder_test.go └── geocoder.go ├── baidu ├── baidu_test.go └── baidu.go ├── tomtom ├── geocoder.go └── geocoder_test.go ├── mapbox └── geocoder.go ├── frenchapigouv ├── geocoder.go └── geocoder_test.go ├── amap ├── amap_test.go └── amap.go ├── google ├── geocoder.go └── geocoder_test.go ├── http_geocoder.go ├── yandex └── geocoder.go ├── examples └── geocoder_example.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/codingsince1985/geo-golang 2 | 3 | go 1.24.1 4 | 5 | require ( 6 | github.com/patrickmn/go-cache v2.1.0+incompatible 7 | github.com/stretchr/testify v1.10.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 4 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 8 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2016 codingsince1985 / Jerry Zhao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /chained/geocoder.go: -------------------------------------------------------------------------------- 1 | package chained 2 | 3 | import ( 4 | "github.com/codingsince1985/geo-golang" 5 | ) 6 | 7 | type chainedGeocoder struct{ Geocoders []geo.Geocoder } 8 | 9 | // Geocoder creates a chain of Geocoders to lookup address and fallback on 10 | func Geocoder(geocoders ...geo.Geocoder) geo.Geocoder { return chainedGeocoder{Geocoders: geocoders} } 11 | 12 | // Geocode returns location for address 13 | func (c chainedGeocoder) Geocode(address string) (*geo.Location, error) { 14 | // Geocode address by each geocoder until we get a real location response 15 | for i := range c.Geocoders { 16 | if l, err := c.Geocoders[i].Geocode(address); err == nil && l != nil { 17 | return l, nil 18 | } 19 | // skip error and try the next geocoder 20 | continue 21 | } 22 | // No geocoders found a result 23 | return nil, nil 24 | } 25 | 26 | // ReverseGeocode returns address for location 27 | func (c chainedGeocoder) ReverseGeocode(lat, lng float64) (*geo.Address, error) { 28 | // Geocode address by each geocoder until we get a real location response 29 | for i := range c.Geocoders { 30 | if addr, err := c.Geocoders[i].ReverseGeocode(lat, lng); err == nil && addr != nil { 31 | return addr, nil 32 | } 33 | // skip error and try the next geocoder 34 | continue 35 | } 36 | // No geocoders found a result 37 | return nil, nil 38 | } 39 | -------------------------------------------------------------------------------- /data/geocoder.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import ( 4 | "github.com/codingsince1985/geo-golang" 5 | ) 6 | 7 | // AddressToLocation maps address string to location (lat, long) 8 | type AddressToLocation map[geo.Address]geo.Location 9 | 10 | // LocationToAddress maps location(lat,lng) to address 11 | type LocationToAddress map[geo.Location]geo.Address 12 | 13 | // dataGeocoder represents geo data in memory 14 | type dataGeocoder struct { 15 | AddressToLocation 16 | LocationToAddress 17 | } 18 | 19 | // Geocoder constructs data geocoder 20 | func Geocoder(addressToLocation AddressToLocation, LocationToAddress LocationToAddress) geo.Geocoder { 21 | return dataGeocoder{ 22 | AddressToLocation: addressToLocation, 23 | LocationToAddress: LocationToAddress, 24 | } 25 | } 26 | 27 | // Geocode returns location for address 28 | func (d dataGeocoder) Geocode(address string) (*geo.Location, error) { 29 | addr := geo.Address{ 30 | FormattedAddress: address, 31 | } 32 | if l, ok := d.AddressToLocation[addr]; ok { 33 | return &l, nil 34 | } 35 | 36 | return nil, nil 37 | } 38 | 39 | // ReverseGeocode returns address for location 40 | func (d dataGeocoder) ReverseGeocode(lat, lng float64) (*geo.Address, error) { 41 | if address, ok := d.LocationToAddress[geo.Location{Lat: lat, Lng: lng}]; ok { 42 | return &address, nil 43 | } 44 | return nil, nil 45 | } 46 | -------------------------------------------------------------------------------- /data/geocoder_test.go: -------------------------------------------------------------------------------- 1 | package data_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/codingsince1985/geo-golang" 8 | "github.com/codingsince1985/geo-golang/data" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | var ( 13 | addressFixture = geo.Address{ 14 | FormattedAddress: "64 Elizabeth Street, Melbourne, Victoria 3000, Australia", 15 | } 16 | locationFixture = geo.Location{ 17 | Lat: -37.814107, 18 | Lng: 144.96328, 19 | } 20 | geocoder = data.Geocoder( 21 | data.AddressToLocation{ 22 | addressFixture: locationFixture, 23 | }, 24 | data.LocationToAddress{ 25 | locationFixture: addressFixture, 26 | }, 27 | ) 28 | ) 29 | 30 | func TestGeocode(t *testing.T) { 31 | location, err := geocoder.Geocode(addressFixture.FormattedAddress) 32 | assert.NoError(t, err) 33 | assert.Equal(t, geo.Location{Lat: -37.814107, Lng: 144.96328}, *location) 34 | } 35 | 36 | func TestReverseGeocode(t *testing.T) { 37 | address, err := geocoder.ReverseGeocode(locationFixture.Lat, locationFixture.Lng) 38 | assert.Nil(t, err) 39 | assert.NotNil(t, address) 40 | assert.True(t, strings.Contains(address.FormattedAddress, "Melbourne, Victoria 3000, Australia")) 41 | } 42 | 43 | func TestReverseGeocodeWithNoResult(t *testing.T) { 44 | addr, err := geocoder.ReverseGeocode(1, 2) 45 | assert.Nil(t, err) 46 | assert.Nil(t, addr) 47 | } 48 | -------------------------------------------------------------------------------- /cached/geocoder.go: -------------------------------------------------------------------------------- 1 | package cached 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/codingsince1985/geo-golang" 7 | "github.com/patrickmn/go-cache" 8 | ) 9 | 10 | type cachedGeocoder struct { 11 | Geocoder geo.Geocoder 12 | Cache *cache.Cache 13 | } 14 | 15 | // Geocoder creates a chain of Geocoders to lookup address and fallback on 16 | func Geocoder(geocoder geo.Geocoder, cache *cache.Cache) geo.Geocoder { 17 | return cachedGeocoder{Geocoder: geocoder, Cache: cache} 18 | } 19 | 20 | // Geocode returns location for address 21 | func (c cachedGeocoder) Geocode(address string) (*geo.Location, error) { 22 | // Check if we've cached this response 23 | if cachedLoc, found := c.Cache.Get(address); found { 24 | return cachedLoc.(*geo.Location), nil 25 | } 26 | 27 | if loc, err := c.Geocoder.Geocode(address); err != nil { 28 | return loc, err 29 | } else { 30 | c.Cache.Set(address, loc, 0) 31 | return loc, nil 32 | } 33 | } 34 | 35 | // ReverseGeocode returns address for location 36 | func (c cachedGeocoder) ReverseGeocode(lat, lng float64) (*geo.Address, error) { 37 | // Check if we've cached this response 38 | locKey := fmt.Sprintf("geo.Location{%f,%f}", lat, lng) 39 | if cachedAddr, found := c.Cache.Get(locKey); found { 40 | return cachedAddr.(*geo.Address), nil 41 | } 42 | 43 | if addr, err := c.Geocoder.ReverseGeocode(lat, lng); err != nil { 44 | return nil, err 45 | } else { 46 | c.Cache.Set(locKey, addr, 0) 47 | return addr, nil 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /geocoder.go: -------------------------------------------------------------------------------- 1 | // Package geo is a generic framework to develop geocode/reverse geocode clients 2 | package geo 3 | 4 | import ( 5 | "io" 6 | "log" 7 | ) 8 | 9 | // Geocoder can look up (lat, long) by address and address by (lat, long) 10 | type Geocoder interface { 11 | Geocode(address string) (*Location, error) 12 | ReverseGeocode(lat, lng float64) (*Address, error) 13 | } 14 | 15 | // Location is the output of Geocode 16 | type Location struct { 17 | Lat, Lng float64 18 | } 19 | 20 | // Address is returned by ReverseGeocode. 21 | // This is a structured representation of an address, including its flat representation 22 | type Address struct { 23 | FormattedAddress string 24 | Street string 25 | HouseNumber string 26 | Suburb string 27 | Postcode string 28 | State string 29 | StateCode string 30 | StateDistrict string 31 | County string 32 | Country string 33 | CountryCode string 34 | City string 35 | } 36 | 37 | // ErrLogger is an implementation of StdLogger that geo uses to log its error messages. 38 | var ErrLogger StdLogger = log.New(io.Discard, "[Geo][Err]", log.LstdFlags) 39 | 40 | // DebugLogger is an implementation of StdLogger that geo uses to log its debug messages. 41 | var DebugLogger StdLogger = log.New(io.Discard, "[Geo][Debug]", log.LstdFlags) 42 | 43 | // StdLogger is a interface for logging libraries. 44 | type StdLogger interface { 45 | Printf(string, ...interface{}) 46 | } 47 | -------------------------------------------------------------------------------- /osm/osm.go: -------------------------------------------------------------------------------- 1 | // Package osm provides common types for OpenStreetMap used by various providers 2 | // and some helper functions to reduce code repetition across specific client implementations. 3 | package osm 4 | 5 | // Address contains address fields specific to OpenStreetMap 6 | type Address struct { 7 | HouseNumber string `json:"house_number"` 8 | Road string `json:"road"` 9 | Pedestrian string `json:"pedestrian"` 10 | Footway string `json:"footway"` 11 | Cycleway string `json:"cycleway"` 12 | Highway string `json:"highway"` 13 | Path string `json:"path"` 14 | Suburb string `json:"suburb"` 15 | City string `json:"city"` 16 | Town string `json:"town"` 17 | Village string `json:"village"` 18 | Hamlet string `json:"hamlet"` 19 | County string `json:"county"` 20 | Country string `json:"country"` 21 | CountryCode string `json:"country_code"` 22 | State string `json:"state"` 23 | StateDistrict string `json:"state_district"` 24 | Postcode string `json:"postcode"` 25 | } 26 | 27 | // Locality checks different fields for the locality name 28 | func (a Address) Locality() string { 29 | var locality string 30 | 31 | if a.City != "" { 32 | locality = a.City 33 | } else if a.Town != "" { 34 | locality = a.Town 35 | } else if a.Village != "" { 36 | locality = a.Village 37 | } else if a.Hamlet != "" { 38 | locality = a.Hamlet 39 | } 40 | 41 | return locality 42 | } 43 | 44 | // Street checks different fields for the street name 45 | func (a Address) Street() string { 46 | var street string 47 | 48 | if a.Road != "" { 49 | street = a.Road 50 | } else if a.Pedestrian != "" { 51 | street = a.Pedestrian 52 | } else if a.Path != "" { 53 | street = a.Path 54 | } else if a.Cycleway != "" { 55 | street = a.Cycleway 56 | } else if a.Footway != "" { 57 | street = a.Footway 58 | } else if a.Highway != "" { 59 | street = a.Highway 60 | } 61 | 62 | return street 63 | } 64 | -------------------------------------------------------------------------------- /geocod/geocoder.go: -------------------------------------------------------------------------------- 1 | package geocod 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | geo "github.com/codingsince1985/geo-golang" 8 | ) 9 | 10 | type ( 11 | baseURL string 12 | geocodeResponse struct { 13 | Results []struct { 14 | Components struct { 15 | Number string 16 | Street string 17 | City string 18 | County string 19 | State string 20 | Zip string 21 | Country string 22 | } `json:"address_components"` 23 | Address string `json:"formatted_address"` 24 | Location struct { 25 | Lat float64 26 | Lng float64 27 | } 28 | } 29 | } 30 | ) 31 | 32 | // Geocoder constructs Geocodio geocoder 33 | func Geocoder(key string, baseURLs ...string) geo.Geocoder { 34 | return geo.HTTPGeocoder{ 35 | EndpointBuilder: baseURL(getUrl(key, baseURLs...)), 36 | ResponseParserFactory: func() geo.ResponseParser { return &geocodeResponse{} }, 37 | } 38 | } 39 | 40 | func getUrl(key string, baseURLs ...string) string { 41 | if len(baseURLs) > 0 { 42 | return baseURLs[0] 43 | } 44 | return "https://api.geocod.io/v1/*&api_key=" + key 45 | } 46 | 47 | func (b baseURL) GeocodeURL(address string) string { 48 | params := fmt.Sprintf("geocode?q=%s", address) 49 | url := strings.Replace(string(b), "*", params, 1) 50 | return url 51 | } 52 | 53 | func (b baseURL) ReverseGeocodeURL(l geo.Location) string { 54 | params := fmt.Sprintf("reverse?q=%f,%f", l.Lat, l.Lng) 55 | url := strings.Replace(string(b), "*", params, 1) 56 | return url 57 | } 58 | 59 | func (r *geocodeResponse) Location() (*geo.Location, error) { 60 | if len(r.Results) == 0 { 61 | return nil, nil 62 | } 63 | 64 | loc := r.Results[0].Location 65 | return &geo.Location{Lat: loc.Lat, Lng: loc.Lng}, nil 66 | } 67 | 68 | func (r *geocodeResponse) Address() (*geo.Address, error) { 69 | if len(r.Results) == 0 { 70 | return nil, nil 71 | } 72 | 73 | r0 := r.Results[0] 74 | c := r0.Components 75 | addr := &geo.Address{ 76 | FormattedAddress: r0.Address, 77 | Street: c.Street, 78 | HouseNumber: c.Number, 79 | Postcode: c.Zip, 80 | State: c.State, 81 | StateCode: c.State, 82 | CountryCode: c.Country, 83 | } 84 | 85 | return addr, nil 86 | } 87 | -------------------------------------------------------------------------------- /mapquest/nominatim/geocoder.go: -------------------------------------------------------------------------------- 1 | // Package nominatim is a geo-golang based MapRequest Nominatim geocode/reverse geocode client 2 | package nominatim 3 | 4 | import ( 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/codingsince1985/geo-golang" 9 | "github.com/codingsince1985/geo-golang/osm" 10 | ) 11 | 12 | type ( 13 | baseURL string 14 | 15 | geocodeResponse struct { 16 | DisplayName string `json:"display_name"` 17 | Lat, Lon, Error string 18 | Addr osm.Address `json:"address"` 19 | } 20 | ) 21 | 22 | var key string 23 | 24 | // Geocoder constructs MapRequest Nominatim geocoder 25 | func Geocoder(k string, baseURLs ...string) geo.Geocoder { 26 | key = k 27 | return geo.HTTPGeocoder{ 28 | EndpointBuilder: baseURL(getURL(baseURLs...)), 29 | ResponseParserFactory: func() geo.ResponseParser { return &geocodeResponse{} }, 30 | } 31 | } 32 | 33 | func getURL(baseURLs ...string) string { 34 | if len(baseURLs) > 0 { 35 | return baseURLs[0] 36 | } 37 | return "http://open.mapquestapi.com/nominatim/v1/" 38 | } 39 | 40 | func (b baseURL) GeocodeURL(address string) string { 41 | return string(b) + "search.php?key=" + key + "&format=json&limit=1&q=" + address 42 | } 43 | 44 | func (b baseURL) ReverseGeocodeURL(l geo.Location) string { 45 | return string(b) + "reverse.php?key=" + key + fmt.Sprintf("&format=json&lat=%f&lon=%f", l.Lat, l.Lng) 46 | } 47 | 48 | func (r *geocodeResponse) Location() (*geo.Location, error) { 49 | if r.Error != "" { 50 | return nil, fmt.Errorf("geocode error: %s", r.Error) 51 | } 52 | 53 | return &geo.Location{ 54 | Lat: geo.ParseFloat(r.Lat), 55 | Lng: geo.ParseFloat(r.Lon), 56 | }, nil 57 | } 58 | 59 | func (r *geocodeResponse) Address() (*geo.Address, error) { 60 | if r.Error != "" { 61 | return nil, fmt.Errorf("reverse geocode error: %s", r.Error) 62 | } 63 | 64 | return &geo.Address{ 65 | FormattedAddress: r.DisplayName, 66 | HouseNumber: r.Addr.HouseNumber, 67 | Street: r.Addr.Street(), 68 | Suburb: r.Addr.Suburb, 69 | City: r.Addr.Locality(), 70 | State: r.Addr.State, 71 | County: r.Addr.County, 72 | Postcode: r.Addr.Postcode, 73 | Country: r.Addr.Country, 74 | CountryCode: strings.ToUpper(r.Addr.CountryCode), 75 | }, nil 76 | } 77 | -------------------------------------------------------------------------------- /openstreetmap/geocoder.go: -------------------------------------------------------------------------------- 1 | // Package openstreetmap is a geo-golang based OpenStreetMap geocode/reverse geocode client 2 | package openstreetmap 3 | 4 | import ( 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/codingsince1985/geo-golang" 9 | "github.com/codingsince1985/geo-golang/osm" 10 | ) 11 | 12 | type ( 13 | baseURL string 14 | geocodeResponse struct { 15 | DisplayName string `json:"display_name"` 16 | Lat string 17 | Lon string 18 | Error string 19 | Addr osm.Address `json:"address"` 20 | } 21 | ) 22 | 23 | // Geocoder constructs OpenStreetMap geocoder 24 | func Geocoder() geo.Geocoder { return GeocoderWithURL("https://nominatim.openstreetmap.org/") } 25 | 26 | // GeocoderWithURL constructs OpenStreetMap geocoder using a custom installation of Nominatim 27 | func GeocoderWithURL(nominatimURL string) geo.Geocoder { 28 | return geo.HTTPGeocoder{ 29 | EndpointBuilder: baseURL(nominatimURL), 30 | ResponseParserFactory: func() geo.ResponseParser { return &geocodeResponse{} }, 31 | } 32 | } 33 | 34 | func (b baseURL) GeocodeURL(address string) string { 35 | return string(b) + "search?format=json&limit=1&q=" + address 36 | } 37 | 38 | func (b baseURL) ReverseGeocodeURL(l geo.Location) string { 39 | return string(b) + "reverse?" + fmt.Sprintf("format=json&lat=%f&lon=%f", l.Lat, l.Lng) 40 | } 41 | 42 | func (r *geocodeResponse) Location() (*geo.Location, error) { 43 | if r.Error != "" { 44 | return nil, fmt.Errorf("geocoding error: %s", r.Error) 45 | } 46 | if r.Lat == "" && r.Lon == "" { 47 | return nil, nil 48 | } 49 | 50 | return &geo.Location{ 51 | Lat: geo.ParseFloat(r.Lat), 52 | Lng: geo.ParseFloat(r.Lon), 53 | }, nil 54 | } 55 | 56 | func (r *geocodeResponse) Address() (*geo.Address, error) { 57 | if r.Error != "" { 58 | return nil, fmt.Errorf("reverse geocoding error: %s", r.Error) 59 | } 60 | 61 | return &geo.Address{ 62 | FormattedAddress: r.DisplayName, 63 | HouseNumber: r.Addr.HouseNumber, 64 | Street: r.Addr.Street(), 65 | Postcode: r.Addr.Postcode, 66 | City: r.Addr.Locality(), 67 | Suburb: r.Addr.Suburb, 68 | State: r.Addr.State, 69 | Country: r.Addr.Country, 70 | CountryCode: strings.ToUpper(r.Addr.CountryCode), 71 | }, nil 72 | } 73 | -------------------------------------------------------------------------------- /pickpoint/geocoder.go: -------------------------------------------------------------------------------- 1 | // Package pickpoint is a geo-golang based PickPoint geocode/reverse geocode client 2 | package pickpoint 3 | 4 | import ( 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/codingsince1985/geo-golang" 9 | "github.com/codingsince1985/geo-golang/osm" 10 | ) 11 | 12 | type ( 13 | baseURL string 14 | geocodeResponse struct { 15 | DisplayName string `json:"display_name"` 16 | Lat string 17 | Lon string 18 | Error string 19 | Addr osm.Address `json:"address"` 20 | } 21 | ) 22 | 23 | var key string 24 | 25 | // Geocoder constructs PickPoint geocoder 26 | func Geocoder(apiKey string, baseURLs ...string) geo.Geocoder { 27 | key = apiKey 28 | return geo.HTTPGeocoder{ 29 | EndpointBuilder: baseURL(getURL(baseURLs...)), 30 | ResponseParserFactory: func() geo.ResponseParser { return &geocodeResponse{} }, 31 | } 32 | } 33 | 34 | func getURL(baseURLs ...string) string { 35 | if len(baseURLs) > 0 { 36 | return baseURLs[0] 37 | } 38 | return "https://api.pickpoint.io/v1" 39 | } 40 | 41 | func (b baseURL) GeocodeURL(address string) string { 42 | return string(b) + fmt.Sprintf("/forward?key=%s&limit=1&q=%s", key, address) 43 | } 44 | 45 | func (b baseURL) ReverseGeocodeURL(l geo.Location) string { 46 | return string(b) + fmt.Sprintf("/reverse?key=%s&lat=%f&lon=%f", key, l.Lat, l.Lng) 47 | } 48 | 49 | func (r *geocodeResponse) Location() (*geo.Location, error) { 50 | if r.Error != "" { 51 | return nil, fmt.Errorf("geocoding error: %s", r.Error) 52 | } 53 | if r.Lat == "" && r.Lon == "" { 54 | return nil, nil 55 | } 56 | 57 | return &geo.Location{ 58 | Lat: geo.ParseFloat(r.Lat), 59 | Lng: geo.ParseFloat(r.Lon), 60 | }, nil 61 | } 62 | 63 | func (r *geocodeResponse) Address() (*geo.Address, error) { 64 | if r.Error != "" { 65 | return nil, fmt.Errorf("reverse geocoding error: %s", r.Error) 66 | } 67 | 68 | return &geo.Address{ 69 | FormattedAddress: r.DisplayName, 70 | HouseNumber: r.Addr.HouseNumber, 71 | Street: r.Addr.Street(), 72 | Postcode: r.Addr.Postcode, 73 | City: r.Addr.Locality(), 74 | Suburb: r.Addr.Suburb, 75 | State: r.Addr.State, 76 | Country: r.Addr.Country, 77 | CountryCode: strings.ToUpper(r.Addr.CountryCode), 78 | }, nil 79 | } 80 | -------------------------------------------------------------------------------- /bing/geocoder.go: -------------------------------------------------------------------------------- 1 | // Package bing is a geo-golang based Microsoft Bing geocode/reverse geocode client 2 | package bing 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/codingsince1985/geo-golang" 10 | ) 11 | 12 | type ( 13 | baseURL string 14 | geocodeResponse struct { 15 | ResourceSets []struct { 16 | Resources []struct { 17 | Point struct { 18 | Coordinates []float64 19 | } 20 | Address struct { 21 | FormattedAddress string 22 | AddressLine string 23 | AdminDistrict string 24 | AdminDistrict2 string 25 | CountryRegion string 26 | Locality string 27 | PostalCode string 28 | } 29 | } 30 | } 31 | ErrorDetails []string 32 | } 33 | ) 34 | 35 | // Geocoder constructs Bing geocoder 36 | func Geocoder(key string, baseURLs ...string) geo.Geocoder { 37 | return geo.HTTPGeocoder{ 38 | EndpointBuilder: baseURL(getURL(key, baseURLs...)), 39 | ResponseParserFactory: func() geo.ResponseParser { return &geocodeResponse{} }, 40 | } 41 | } 42 | 43 | func getURL(key string, baseURLs ...string) string { 44 | if len(baseURLs) > 0 { 45 | return baseURLs[0] 46 | } 47 | return "http://dev.virtualearth.net/REST/v1/Locations*key=" + key 48 | } 49 | 50 | func (b baseURL) GeocodeURL(address string) string { 51 | return strings.Replace(string(b), "*", "?q="+address+"&", 1) 52 | } 53 | 54 | func (b baseURL) ReverseGeocodeURL(l geo.Location) string { 55 | return strings.Replace(string(b), "*", fmt.Sprintf("/%f,%f?", l.Lat, l.Lng), 1) 56 | } 57 | 58 | func (r *geocodeResponse) Location() (*geo.Location, error) { 59 | if len(r.ResourceSets) <= 0 || len(r.ResourceSets[0].Resources) <= 0 { 60 | return nil, nil 61 | } 62 | c := r.ResourceSets[0].Resources[0].Point.Coordinates 63 | return &geo.Location{ 64 | Lat: c[0], 65 | Lng: c[1], 66 | }, nil 67 | } 68 | 69 | func (r *geocodeResponse) Address() (*geo.Address, error) { 70 | if len(r.ErrorDetails) > 0 { 71 | return nil, errors.New(strings.Join(r.ErrorDetails, " ")) 72 | } 73 | if len(r.ResourceSets) <= 0 || len(r.ResourceSets[0].Resources) <= 0 { 74 | return nil, nil 75 | } 76 | 77 | a := r.ResourceSets[0].Resources[0].Address 78 | return &geo.Address{ 79 | FormattedAddress: a.FormattedAddress, 80 | Street: a.AddressLine, 81 | City: a.Locality, 82 | Postcode: a.PostalCode, 83 | Country: a.CountryRegion, 84 | }, nil 85 | } 86 | -------------------------------------------------------------------------------- /opencage/geocoder.go: -------------------------------------------------------------------------------- 1 | // Package opencage is a geo-golang based OpenCage geocode/reverse geocode client 2 | package opencage 3 | 4 | import ( 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/codingsince1985/geo-golang" 9 | "github.com/codingsince1985/geo-golang/osm" 10 | ) 11 | 12 | type ( 13 | baseURL string 14 | 15 | geocodeResponse struct { 16 | Results []struct { 17 | Formatted string 18 | Geometry geo.Location 19 | Components osm.Address 20 | } 21 | Status struct { 22 | Code int 23 | Message string 24 | } 25 | } 26 | ) 27 | 28 | // Geocoder constructs OpenCage geocoder 29 | func Geocoder(key string, baseURLs ...string) geo.Geocoder { 30 | return geo.HTTPGeocoder{ 31 | EndpointBuilder: baseURL(getURL(key, baseURLs...)), 32 | ResponseParserFactory: func() geo.ResponseParser { return &geocodeResponse{} }, 33 | } 34 | } 35 | 36 | func getURL(key string, baseURLs ...string) string { 37 | if len(baseURLs) > 0 { 38 | return baseURLs[0] 39 | } 40 | return "http://api.opencagedata.com/geocode/v1/json?key=" + key + "&q=" 41 | } 42 | 43 | func (b baseURL) GeocodeURL(address string) string { return string(b) + address } 44 | 45 | func (b baseURL) ReverseGeocodeURL(l geo.Location) string { 46 | return string(b) + fmt.Sprintf("%+f,%+f", l.Lat, l.Lng) 47 | } 48 | 49 | func (r *geocodeResponse) Location() (*geo.Location, error) { 50 | if r.Status.Code >= 400 { 51 | return nil, fmt.Errorf("geocoding error: %s", r.Status.Message) 52 | } 53 | if len(r.Results) == 0 { 54 | return nil, nil 55 | } 56 | 57 | return &geo.Location{ 58 | Lat: r.Results[0].Geometry.Lat, 59 | Lng: r.Results[0].Geometry.Lng, 60 | }, nil 61 | } 62 | 63 | func (r *geocodeResponse) Address() (*geo.Address, error) { 64 | if r.Status.Code >= 400 { 65 | return nil, fmt.Errorf("geocoding error: %s", r.Status.Message) 66 | } 67 | if len(r.Results) == 0 { 68 | return nil, nil 69 | } 70 | 71 | addr := r.Results[0].Components 72 | 73 | locality := addr.Locality() 74 | if locality == "" { 75 | locality = addr.Suburb 76 | } 77 | return &geo.Address{ 78 | FormattedAddress: r.Results[0].Formatted, 79 | HouseNumber: addr.HouseNumber, 80 | Street: addr.Street(), 81 | Suburb: addr.Suburb, 82 | Postcode: addr.Postcode, 83 | City: locality, 84 | CountryCode: strings.ToUpper(addr.CountryCode), 85 | Country: addr.Country, 86 | County: addr.County, 87 | State: addr.State, 88 | StateDistrict: addr.StateDistrict, 89 | }, nil 90 | } 91 | -------------------------------------------------------------------------------- /arcgis/geocoder.go: -------------------------------------------------------------------------------- 1 | // Package arcgis is a geo-golang based ArcGIS geocode/reverse client 2 | package arcgis 3 | 4 | import ( 5 | "fmt" 6 | "strings" 7 | 8 | geo "github.com/codingsince1985/geo-golang" 9 | ) 10 | 11 | type ( 12 | baseURL string 13 | geocodeResponse struct { 14 | Candidates []struct { 15 | Address string 16 | Location struct { 17 | X float64 18 | Y float64 19 | } 20 | } 21 | 22 | ReverseAddress struct { 23 | MatchAddr string `json:"Match_addr"` 24 | LongLabel string 25 | ShortLabel string 26 | AddNum string 27 | Address string 28 | Neighborhood string 29 | City string 30 | Subregion string 31 | Region string 32 | Postal string 33 | CountryCode string 34 | } `json:"address"` 35 | } 36 | ) 37 | 38 | // Geocoder constructs ArcGIS geocoder 39 | func Geocoder(token string, baseURLs ...string) geo.Geocoder { 40 | return geo.HTTPGeocoder{ 41 | EndpointBuilder: baseURL(getUrl(token, baseURLs...)), 42 | ResponseParserFactory: func() geo.ResponseParser { return &geocodeResponse{} }, 43 | } 44 | } 45 | 46 | func getUrl(token string, baseURLs ...string) string { 47 | if len(baseURLs) > 0 { 48 | return baseURLs[0] 49 | } 50 | 51 | url := "http://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/*" 52 | if len(token) > 0 { 53 | url += "&token=" + token 54 | } 55 | return url 56 | } 57 | 58 | func (b baseURL) GeocodeURL(address string) string { 59 | params := fmt.Sprintf("findAddressCandidates?f=json&maxLocations=%d&SingleLine=%s", 1, address) 60 | return strings.Replace(string(b), "*", params, 1) 61 | } 62 | 63 | func (b baseURL) ReverseGeocodeURL(l geo.Location) string { 64 | params := fmt.Sprintf("reverseGeocode?f=json&location=%f,%f", l.Lng, l.Lat) 65 | return strings.Replace(string(b), "*", params, 1) 66 | } 67 | 68 | func (r *geocodeResponse) Location() (*geo.Location, error) { 69 | if len(r.Candidates) == 0 { 70 | return nil, nil 71 | } 72 | 73 | g := r.Candidates[0].Location 74 | return &geo.Location{ 75 | Lat: g.Y, 76 | Lng: g.X, 77 | }, nil 78 | } 79 | 80 | func (r *geocodeResponse) Address() (*geo.Address, error) { 81 | addr := &geo.Address{ 82 | FormattedAddress: r.ReverseAddress.MatchAddr, 83 | Street: r.ReverseAddress.Address, 84 | HouseNumber: r.ReverseAddress.AddNum, 85 | Postcode: r.ReverseAddress.Postal, 86 | State: r.ReverseAddress.Region, 87 | CountryCode: r.ReverseAddress.CountryCode, 88 | } 89 | 90 | return addr, nil 91 | } 92 | -------------------------------------------------------------------------------- /chained/geocoder_test.go: -------------------------------------------------------------------------------- 1 | package chained_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/codingsince1985/geo-golang" 8 | "github.com/codingsince1985/geo-golang/chained" 9 | "github.com/codingsince1985/geo-golang/data" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | // geocoder is chained with one data geocoder with address -> location data 14 | // the other has location -> address data 15 | // this will exercise the chained fallback handling 16 | var ( 17 | addressFixture = geo.Address{ 18 | FormattedAddress: "64 Elizabeth Street, Melbourne, Victoria 3000, Australia", 19 | } 20 | locationFixture = geo.Location{ 21 | Lat: -37.814107, 22 | Lng: 144.96328, 23 | } 24 | geocoder = chained.Geocoder( 25 | data.Geocoder( 26 | data.AddressToLocation{ 27 | addressFixture: locationFixture, 28 | }, 29 | data.LocationToAddress{}, 30 | ), 31 | 32 | data.Geocoder( 33 | data.AddressToLocation{}, 34 | data.LocationToAddress{ 35 | locationFixture: addressFixture, 36 | }, 37 | ), 38 | ) 39 | ) 40 | 41 | func TestGeocode(t *testing.T) { 42 | location, err := geocoder.Geocode(addressFixture.FormattedAddress) 43 | assert.NoError(t, err) 44 | assert.Equal(t, geo.Location{locationFixture.Lat, locationFixture.Lng}, *location) 45 | } 46 | 47 | func TestReverseGeocode(t *testing.T) { 48 | address, err := geocoder.ReverseGeocode(locationFixture.Lat, locationFixture.Lng) 49 | assert.NoError(t, err) 50 | assert.True(t, strings.HasSuffix(address.FormattedAddress, "Melbourne, Victoria 3000, Australia")) 51 | } 52 | 53 | func TestReverseGeocodeWithNoResult(t *testing.T) { 54 | addr, err := geocoder.ReverseGeocode(0, 0) 55 | assert.Nil(t, err) 56 | assert.Nil(t, addr) 57 | } 58 | 59 | func TestChainedGeocode(t *testing.T) { 60 | mock1 := data.Geocoder( 61 | data.AddressToLocation{ 62 | geo.Address{FormattedAddress: "Austin,TX"}: geo.Location{Lat: 1, Lng: 2}, 63 | }, 64 | data.LocationToAddress{}, 65 | ) 66 | 67 | mock2 := data.Geocoder( 68 | data.AddressToLocation{ 69 | geo.Address{FormattedAddress: "Dallas,TX"}: geo.Location{Lat: 3, Lng: 4}, 70 | }, 71 | data.LocationToAddress{}, 72 | ) 73 | 74 | c := chained.Geocoder(mock1, mock2) 75 | 76 | l, err := c.Geocode("Austin,TX") 77 | assert.NoError(t, err) 78 | assert.Equal(t, geo.Location{Lat: 1, Lng: 2}, *l) 79 | 80 | l, err = c.Geocode("Dallas,TX") 81 | assert.NoError(t, err) 82 | assert.Equal(t, geo.Location{Lat: 3, Lng: 4}, *l) 83 | 84 | addr, err := c.Geocode("NOWHERE,TX") 85 | assert.Nil(t, err) 86 | assert.Nil(t, addr) 87 | } 88 | -------------------------------------------------------------------------------- /cached/geocoder_test.go: -------------------------------------------------------------------------------- 1 | package cached_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "time" 7 | 8 | "github.com/codingsince1985/geo-golang" 9 | "github.com/codingsince1985/geo-golang/cached" 10 | "github.com/codingsince1985/geo-golang/data" 11 | "github.com/patrickmn/go-cache" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | var geoCache = cache.New(5*time.Minute, 30*time.Second) 16 | 17 | // geocoder is chained with one data geocoder with address -> location data 18 | // the other has location -> address data 19 | // this will exercise the chained fallback handling 20 | var ( 21 | addressFixture = geo.Address{ 22 | FormattedAddress: "64 Elizabeth Street, Melbourne, Victoria 3000, Australia", 23 | } 24 | locationFixture = geo.Location{ 25 | Lat: -37.814107, 26 | Lng: 144.96328, 27 | } 28 | geocoder = cached.Geocoder( 29 | data.Geocoder( 30 | data.AddressToLocation{ 31 | addressFixture: locationFixture, 32 | }, 33 | data.LocationToAddress{ 34 | locationFixture: addressFixture, 35 | }, 36 | ), 37 | geoCache, 38 | ) 39 | ) 40 | 41 | func TestGeocode(t *testing.T) { 42 | location, err := geocoder.Geocode("64 Elizabeth Street, Melbourne, Victoria 3000, Australia") 43 | assert.NoError(t, err) 44 | assert.Equal(t, locationFixture, *location) 45 | } 46 | 47 | func TestReverseGeocode(t *testing.T) { 48 | address, err := geocoder.ReverseGeocode(locationFixture.Lat, locationFixture.Lng) 49 | assert.NoError(t, err) 50 | assert.True(t, strings.HasSuffix(address.FormattedAddress, "Melbourne, Victoria 3000, Australia")) 51 | } 52 | 53 | func TestReverseGeocodeWithNoResult(t *testing.T) { 54 | addr, err := geocoder.ReverseGeocode(1, 2) 55 | assert.Nil(t, err) 56 | assert.Nil(t, addr) 57 | } 58 | 59 | func TestCachedGeocode(t *testing.T) { 60 | mockAddr := geo.Address{ 61 | FormattedAddress: "42, Some Street, Austin, Texas", 62 | } 63 | mock1 := data.Geocoder( 64 | data.AddressToLocation{ 65 | mockAddr: geo.Location{Lat: 1, Lng: 2}, 66 | }, 67 | data.LocationToAddress{}, 68 | ) 69 | 70 | c := cached.Geocoder(mock1, geoCache) 71 | 72 | l, err := c.Geocode("42, Some Street, Austin, Texas") 73 | assert.NoError(t, err) 74 | assert.Equal(t, geo.Location{Lat: 1, Lng: 2}, *l) 75 | 76 | // Should be cached 77 | // TODO: write a mock Cache impl to test cache is being used 78 | l, err = c.Geocode("42, Some Street, Austin, Texas") 79 | assert.NoError(t, err) 80 | assert.Equal(t, geo.Location{Lat: 1, Lng: 2}, *l) 81 | 82 | addr, err := c.Geocode("NOWHERE,TX") 83 | assert.Nil(t, err) 84 | assert.Nil(t, addr) 85 | } 86 | -------------------------------------------------------------------------------- /mapzen/geocoder.go: -------------------------------------------------------------------------------- 1 | // Package mapzen is a geo-golang based Mapzen geocode/reverse client 2 | package mapzen 3 | 4 | import ( 5 | "fmt" 6 | "strings" 7 | 8 | geo "github.com/codingsince1985/geo-golang" 9 | ) 10 | 11 | type ( 12 | baseURL string 13 | geocodeResponse struct { 14 | Geocoding struct { 15 | Query struct { 16 | Text string 17 | } 18 | } 19 | 20 | Features []struct { 21 | Geometry struct { 22 | Coordinates []float64 23 | } 24 | Properties struct { 25 | Name string 26 | HouseNumber string 27 | Street string 28 | PostalCode string 29 | Country string 30 | CountryCode string `json:"country_a"` 31 | Region string 32 | RegionCode string `json:"region_a"` 33 | County string 34 | Label string 35 | } 36 | } 37 | } 38 | ) 39 | 40 | // Geocoder constructs Mapzen geocoder 41 | func Geocoder(key string, baseURLs ...string) geo.Geocoder { 42 | return geo.HTTPGeocoder{ 43 | EndpointBuilder: baseURL(getUrl(key, baseURLs...)), 44 | ResponseParserFactory: func() geo.ResponseParser { return &geocodeResponse{} }, 45 | } 46 | } 47 | 48 | func getUrl(key string, baseURLs ...string) string { 49 | if len(baseURLs) > 0 { 50 | return baseURLs[0] 51 | } 52 | return "https://search.mapzen.com/v1/*" + "&api_key=" + key 53 | } 54 | 55 | func (b baseURL) GeocodeURL(address string) string { 56 | params := fmt.Sprintf("search?size=%d&text=%s", 1, address) 57 | return strings.Replace(string(b), "*", params, 1) 58 | } 59 | 60 | func (b baseURL) ReverseGeocodeURL(l geo.Location) string { 61 | params := fmt.Sprintf("reverse?size=%d&point.lat=%f&point.lon=%f", 1, l.Lat, l.Lng) 62 | return strings.Replace(string(b), "*", params, 1) 63 | } 64 | 65 | func (r *geocodeResponse) Location() (*geo.Location, error) { 66 | if len(r.Features) == 0 { 67 | return nil, nil 68 | } 69 | 70 | pt := r.Features[0].Geometry.Coordinates 71 | if len(pt) == 0 { 72 | return nil, nil 73 | } 74 | 75 | return &geo.Location{Lat: pt[1], Lng: pt[0]}, nil 76 | } 77 | 78 | func (r *geocodeResponse) Address() (*geo.Address, error) { 79 | if len(r.Features) == 0 { 80 | return nil, nil 81 | } 82 | 83 | props := r.Features[0].Properties 84 | addr := &geo.Address{ 85 | FormattedAddress: props.Label, 86 | Street: props.Street, 87 | HouseNumber: props.HouseNumber, 88 | Postcode: props.PostalCode, 89 | Country: props.Country, 90 | CountryCode: props.CountryCode, 91 | State: props.Region, 92 | StateCode: props.RegionCode, 93 | } 94 | return addr, nil 95 | } 96 | -------------------------------------------------------------------------------- /mapquest/open/geocoder.go: -------------------------------------------------------------------------------- 1 | // Package open is a geo-golang based MapRequest Open geocode/reverse geocode client 2 | package open 3 | 4 | import ( 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/codingsince1985/geo-golang" 9 | ) 10 | 11 | type ( 12 | baseURL string 13 | geocodeResponse struct { 14 | Results []struct { 15 | Locations []struct { 16 | LatLng struct { 17 | Lat float64 18 | Lng float64 19 | } 20 | PostalCode string 21 | Street string 22 | AdminArea6 string // neighbourhood 23 | AdminArea5 string // city 24 | AdminArea4 string // county 25 | AdminArea3 string // state 26 | AdminArea1 string // country (ISO 3166-1 alpha-2 code) 27 | } 28 | } 29 | } 30 | ) 31 | 32 | // Geocoder constructs MapRequest Open geocoder 33 | func Geocoder(key string, baseURLs ...string) geo.Geocoder { 34 | 35 | return geo.HTTPGeocoder{ 36 | EndpointBuilder: baseURL(getURL(key, baseURLs...)), 37 | ResponseParserFactory: func() geo.ResponseParser { return &geocodeResponse{} }, 38 | } 39 | } 40 | 41 | func getURL(key string, baseURLs ...string) string { 42 | if len(baseURLs) > 0 { 43 | return baseURLs[0] 44 | } 45 | return "http://open.mapquestapi.com/geocoding/v1/*?key=" + key + "&location=" 46 | } 47 | 48 | func (b baseURL) GeocodeURL(address string) string { 49 | return strings.Replace(string(b), "*", "address", 1) + address 50 | } 51 | 52 | func (b baseURL) ReverseGeocodeURL(l geo.Location) string { 53 | return strings.Replace(string(b), "*", "reverse", 1) + fmt.Sprintf("%f,%f", l.Lat, l.Lng) 54 | } 55 | 56 | func (r *geocodeResponse) Location() (*geo.Location, error) { 57 | if len(r.Results) == 0 || len(r.Results[0].Locations) == 0 { 58 | return nil, nil 59 | } 60 | 61 | loc := r.Results[0].Locations[0].LatLng 62 | return &geo.Location{ 63 | Lat: loc.Lat, 64 | Lng: loc.Lng, 65 | }, nil 66 | } 67 | 68 | func (r *geocodeResponse) Address() (*geo.Address, error) { 69 | if len(r.Results) == 0 || len(r.Results[0].Locations) == 0 { 70 | return nil, nil 71 | } 72 | 73 | p := r.Results[0].Locations[0] 74 | if p.Street == "" || p.AdminArea5 == "" { 75 | return nil, nil 76 | } 77 | 78 | formattedAddress := p.Street + ", " + p.PostalCode + ", " + p.AdminArea5 + ", " + p.AdminArea3 + ", " + p.AdminArea1 79 | return &geo.Address{ 80 | FormattedAddress: formattedAddress, 81 | Street: p.Street, 82 | Suburb: p.AdminArea6, 83 | Postcode: p.PostalCode, 84 | City: p.AdminArea5, 85 | County: p.AdminArea4, 86 | State: p.AdminArea3, 87 | CountryCode: p.AdminArea1, 88 | }, nil 89 | } 90 | -------------------------------------------------------------------------------- /locationiq/geocoder.go: -------------------------------------------------------------------------------- 1 | // Package locationiq is a geo-golang based LocationIQ geocode/reverse geocode client 2 | package locationiq 3 | 4 | import ( 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/codingsince1985/geo-golang" 9 | "github.com/codingsince1985/geo-golang/osm" 10 | ) 11 | 12 | type baseURL string 13 | 14 | type geocodeResponse struct { 15 | DisplayName string `json:"display_name"` 16 | Lat, Lon, Error string 17 | Addr osm.Address `json:"address"` 18 | } 19 | 20 | const ( 21 | defaultURL = "http://locationiq.org/v1/" 22 | minZoom = 0 // Min zoom level for locationiq - country level 23 | maxZoom = 18 // Max zoom level for locationiq - house level 24 | defaultZoom = 18 25 | ) 26 | 27 | var ( 28 | key string 29 | zoom int 30 | ) 31 | 32 | // Geocoder constructs LocationIQ geocoder 33 | func Geocoder(k string, z int, baseURLs ...string) geo.Geocoder { 34 | key = k 35 | 36 | var url string 37 | if len(baseURLs) > 0 { 38 | url = baseURLs[0] 39 | } else { 40 | url = defaultURL 41 | } 42 | 43 | if z > minZoom && z <= maxZoom { 44 | zoom = z 45 | } else { 46 | zoom = defaultZoom 47 | } 48 | 49 | return geo.HTTPGeocoder{ 50 | EndpointBuilder: baseURL(url), 51 | ResponseParserFactory: func() geo.ResponseParser { return &geocodeResponse{} }, 52 | } 53 | } 54 | 55 | func (b baseURL) GeocodeURL(address string) string { 56 | return string(b) + "search.php?key=" + key + "&format=json&limit=1&q=" + address 57 | } 58 | 59 | func (b baseURL) ReverseGeocodeURL(l geo.Location) string { 60 | return string(b) + "reverse.php?key=" + key + fmt.Sprintf("&format=json&lat=%f&lon=%f&zoom=%d", l.Lat, l.Lng, zoom) 61 | } 62 | 63 | func (r *geocodeResponse) Location() (*geo.Location, error) { 64 | if r.Error != "" { 65 | return nil, fmt.Errorf("geocoding error: %s", r.Error) 66 | } 67 | 68 | // no result 69 | if r.Lat == "" || r.Lon == "" { 70 | return nil, nil 71 | } 72 | 73 | return &geo.Location{ 74 | Lat: geo.ParseFloat(r.Lat), 75 | Lng: geo.ParseFloat(r.Lon), 76 | }, nil 77 | } 78 | 79 | func (r *geocodeResponse) Address() (*geo.Address, error) { 80 | if r.Error != "" { 81 | return nil, fmt.Errorf("reverse geocoding error: %s", r.Error) 82 | } 83 | 84 | return &geo.Address{ 85 | FormattedAddress: r.DisplayName, 86 | Street: r.Addr.Street(), 87 | HouseNumber: r.Addr.HouseNumber, 88 | City: r.Addr.Locality(), 89 | Postcode: r.Addr.Postcode, 90 | Suburb: r.Addr.Suburb, 91 | State: r.Addr.State, 92 | Country: r.Addr.Country, 93 | CountryCode: strings.ToUpper(r.Addr.CountryCode), 94 | }, nil 95 | } 96 | -------------------------------------------------------------------------------- /here/search/geocoder.go: -------------------------------------------------------------------------------- 1 | // Package search is a geo-golang based HERE geocode/reverse geocode client for the Geocoding and Search API 2 | package search 3 | 4 | import ( 5 | "fmt" 6 | "net/url" 7 | 8 | "github.com/codingsince1985/geo-golang" 9 | ) 10 | 11 | type ( 12 | baseURL struct{ forGeocode, forReverseGeocode string } 13 | geocodeResponse struct { 14 | Items []struct { 15 | Address struct { 16 | Label string 17 | CountryCode string 18 | CountryName string 19 | StateCode string 20 | State string 21 | County string 22 | District string 23 | City string 24 | Street string 25 | PostalCode string 26 | HouseNumber string 27 | } 28 | Position struct { 29 | Lat float64 30 | Lng float64 31 | } 32 | } 33 | } 34 | ) 35 | 36 | // Geocoder constructs HERE geocoder 37 | func Geocoder(apiKey string, baseURLs ...string) geo.Geocoder { 38 | p := "apiKey=" + url.QueryEscape(apiKey) 39 | return geo.HTTPGeocoder{ 40 | EndpointBuilder: baseURL{ 41 | getGeocodeURL(p, baseURLs...), 42 | getReverseGeocodeURL(p, baseURLs...)}, 43 | ResponseParserFactory: func() geo.ResponseParser { return &geocodeResponse{} }, 44 | } 45 | } 46 | 47 | func getGeocodeURL(p string, baseURLs ...string) string { 48 | if len(baseURLs) > 0 { 49 | return baseURLs[0] 50 | } 51 | return "https://geocode.search.hereapi.com/v1/geocode?" + p 52 | } 53 | 54 | func getReverseGeocodeURL(p string, baseURLs ...string) string { 55 | if len(baseURLs) > 0 { 56 | return baseURLs[0] 57 | } 58 | return "https://revgeocode.search.hereapi.com/v1/revgeocode?" + p 59 | } 60 | 61 | func (b baseURL) GeocodeURL(address string) string { return b.forGeocode + "&limit=1&q=" + address } 62 | 63 | func (b baseURL) ReverseGeocodeURL(l geo.Location) string { 64 | return b.forReverseGeocode + fmt.Sprintf("&limit=1&at=%f,%f", l.Lat, l.Lng) 65 | } 66 | 67 | func (r *geocodeResponse) Location() (*geo.Location, error) { 68 | if len(r.Items) == 0 { 69 | return nil, nil 70 | } 71 | p := r.Items[0].Position 72 | return &geo.Location{ 73 | Lat: p.Lat, 74 | Lng: p.Lng, 75 | }, nil 76 | } 77 | 78 | func (r *geocodeResponse) Address() (*geo.Address, error) { 79 | if len(r.Items) == 0 { 80 | return nil, nil 81 | } 82 | 83 | res := r.Items[0].Address 84 | 85 | addr := &geo.Address{ 86 | FormattedAddress: res.Label, 87 | City: res.City, 88 | Street: res.Street, 89 | HouseNumber: res.HouseNumber, 90 | Postcode: res.PostalCode, 91 | State: res.State, 92 | County: res.County, 93 | Country: res.CountryName, 94 | CountryCode: res.CountryCode, 95 | Suburb: res.District, 96 | } 97 | return addr, nil 98 | } 99 | -------------------------------------------------------------------------------- /baidu/baidu_test.go: -------------------------------------------------------------------------------- 1 | package baidu_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/codingsince1985/geo-golang" 11 | "github.com/codingsince1985/geo-golang/baidu" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | var key = os.Getenv("BAIDU_APP_KEY") 16 | 17 | func TestGeocode(t *testing.T) { 18 | ts := testServer(response1) 19 | defer ts.Close() 20 | 21 | geocoder := baidu.Geocoder(key, "en", "bd09ll", ts.URL+"/") 22 | location, err := geocoder.Geocode("60 Collins St, Melbourne VIC") 23 | 24 | assert.NoError(t, err) 25 | assert.Equal(t, geo.Location{Lat: 40.05703033345938, Lng: 116.3084202915042}, *location) 26 | } 27 | 28 | func TestReverseGeocode(t *testing.T) { 29 | ts := testServer(response2) 30 | defer ts.Close() 31 | 32 | geocoder := baidu.Geocoder(key, "en", "bd09ll", ts.URL+"/") 33 | address, err := geocoder.ReverseGeocode(40.03333340036988, 116.29999999999993) 34 | 35 | assert.NoError(t, err) 36 | assert.True(t, strings.HasPrefix(address.FormattedAddress, "43号 农大南路, Haidian, Beijing, China")) 37 | assert.True(t, address.City == "Beijing") 38 | } 39 | 40 | func TestReverseGeocodeWithNoResult(t *testing.T) { 41 | ts := testServer(response3) 42 | defer ts.Close() 43 | 44 | geocoder := baidu.Geocoder(key, "en", "bd09ll", ts.URL+"/") 45 | addr, err := geocoder.ReverseGeocode(-37.81375, 164.97176) 46 | 47 | assert.NoError(t, err) 48 | assert.Nil(t, addr) 49 | } 50 | 51 | func testServer(response string) *httptest.Server { 52 | return httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 53 | resp.Write([]byte(response)) 54 | })) 55 | } 56 | 57 | const ( 58 | response1 = `{"status":0,"result":{"location":{"lng":116.3084202915042,"lat":40.05703033345938},"precise":1,"confidence":80,"comprehension":100,"level":"门址"}}` 59 | response2 = `{"status":0,"result":{"location":{"lng":116.29999999999993,"lat":40.03333340036988},"formatted_address":"43号 农大南路, Haidian, Beijing, China","business":"马连洼,上地","addressComponent":{"country":"China","country_code":0,"country_code_iso":"CHN","country_code_iso2":"CN","province":"Beijing","city":"Beijing","city_level":2,"district":"Haidian","town":"","town_code":"","adcode":"110108","street":"农大南路","street_number":"43号","direction":"附近","distance":"26"},"pois":[],"roads":[],"poiRegions":[],"sematic_description":"","cityCode":131}}` 60 | response3 = `{"status":0,"result":{"location":{"lng":164.97175999999986,"lat":-37.81375002268602},"formatted_address":"","business":"","addressComponent":{"country":"","country_code":-1,"country_code_iso":"","country_code_iso2":"","province":"","city":"","city_level":2,"district":"","town":"","town_code":"","adcode":"0","street":"","street_number":"","direction":"","distance":""},"pois":[],"roads":[],"poiRegions":[],"sematic_description":"","cityCode":0}}` 61 | ) 62 | -------------------------------------------------------------------------------- /tomtom/geocoder.go: -------------------------------------------------------------------------------- 1 | // Package tomtom is a geo-golang based TomTom geocode/reverse geocode client 2 | package tomtom 3 | 4 | import ( 5 | "fmt" 6 | "strings" 7 | 8 | geo "github.com/codingsince1985/geo-golang" 9 | ) 10 | 11 | type ( 12 | baseURL string 13 | geocodeResponse struct { 14 | Summary struct { 15 | Query string 16 | } 17 | 18 | Results []struct { 19 | Position struct { 20 | Lat float64 21 | Lon float64 22 | } 23 | } 24 | 25 | // Reverse Geocoding response 26 | Addresses []struct { 27 | Address struct { 28 | BuildingNumber string 29 | StreetNumber string 30 | Street string 31 | StreetName string 32 | StreetNameAndNumber string 33 | CountryCode string 34 | CountrySubdivision string // state code 35 | CountrySecondarySubdivision string 36 | CountryTertiarySubdivision string 37 | Municipality string // city 38 | PostalCode string 39 | Country string 40 | CountryCodeISO3 string 41 | FreeformAddress string 42 | CountrySubdivisionName string 43 | } 44 | } 45 | } 46 | ) 47 | 48 | // Geocoder constructs TomTom geocoder 49 | func Geocoder(key string, baseURLs ...string) geo.Geocoder { 50 | return geo.HTTPGeocoder{ 51 | EndpointBuilder: baseURL(getUrl(key, baseURLs...)), 52 | ResponseParserFactory: func() geo.ResponseParser { return &geocodeResponse{} }, 53 | } 54 | } 55 | 56 | func getUrl(key string, baseURLs ...string) string { 57 | if len(baseURLs) > 0 { 58 | return baseURLs[0] 59 | } 60 | return "https://api.tomtom.com/search/2/*?key=" + key 61 | } 62 | 63 | func (b baseURL) GeocodeURL(address string) string { 64 | params := fmt.Sprintf("geocode/%s.json", address) 65 | return strings.Replace(string(b), "*", params, 1) 66 | } 67 | 68 | func (b baseURL) ReverseGeocodeURL(l geo.Location) string { 69 | params := fmt.Sprintf("reverseGeocode/%f,%f", l.Lat, l.Lng) 70 | return strings.Replace(string(b), "*", params, 1) 71 | } 72 | 73 | func (r *geocodeResponse) Location() (*geo.Location, error) { 74 | if len(r.Results) > 0 { 75 | p := r.Results[0].Position 76 | 77 | return &geo.Location{ 78 | Lat: p.Lat, 79 | Lng: p.Lon, 80 | }, nil 81 | } 82 | return nil, nil 83 | } 84 | 85 | func (r *geocodeResponse) Address() (*geo.Address, error) { 86 | if len(r.Addresses) > 0 { 87 | a := r.Addresses[0].Address 88 | return &geo.Address{ 89 | FormattedAddress: a.FreeformAddress, 90 | Street: a.StreetName, 91 | HouseNumber: a.StreetNumber, 92 | City: a.Municipality, 93 | Postcode: a.PostalCode, 94 | State: a.CountrySubdivision, 95 | Country: a.Country, 96 | CountryCode: a.CountryCode, 97 | }, nil 98 | } 99 | return nil, nil 100 | } 101 | -------------------------------------------------------------------------------- /pickpoint/geocoder_test.go: -------------------------------------------------------------------------------- 1 | package pickpoint_test 2 | 3 | import ( 4 | "github.com/codingsince1985/geo-golang" 5 | "github.com/codingsince1985/geo-golang/pickpoint" 6 | "github.com/stretchr/testify/assert" 7 | "net/http" 8 | "net/http/httptest" 9 | "os" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | var key = os.Getenv("PICKPOINT_API_KEY") 15 | 16 | func TestGeocode(t *testing.T) { 17 | ts := testServer(response1) 18 | defer ts.Close() 19 | 20 | geocoder := pickpoint.Geocoder(key, ts.URL+"/") 21 | location, err := geocoder.Geocode("60 Collins St, Melbourne VIC 3000") 22 | assert.Nil(t, err) 23 | assert.Equal(t, geo.Location{Lat: -37.8157915, Lng: 144.9656171}, *location) 24 | } 25 | 26 | func TestReverseGeocode(t *testing.T) { 27 | ts := testServer(response2) 28 | defer ts.Close() 29 | 30 | geocoder := pickpoint.Geocoder(key, ts.URL+"/") 31 | address, err := geocoder.ReverseGeocode(-37.8157915, 144.9656171) 32 | assert.Nil(t, err) 33 | assert.True(t, strings.Index(address.FormattedAddress, "Collins St") > 0) 34 | } 35 | 36 | func TestReverseGeocodeWithNoResult(t *testing.T) { 37 | ts := testServer(response3) 38 | defer ts.Close() 39 | 40 | geocoder := pickpoint.Geocoder(key, ts.URL+"/") 41 | addr, err := geocoder.ReverseGeocode(-37.8157915, 164.9656171) 42 | assert.Nil(t, addr) 43 | assert.NotNil(t, err) 44 | } 45 | 46 | func testServer(response string) *httptest.Server { 47 | return httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 48 | resp.Write([]byte(response)) 49 | })) 50 | } 51 | 52 | const ( 53 | response1 = `[ 54 | { 55 | "place_id":"133372311", 56 | "licence":"Data © OpenStreetMap contributors, ODbL 1.0. http:\/\/www.openstreetmap.org\/copyright", 57 | "osm_type":"way", 58 | "osm_id":"316166613", 59 | "boundingbox":[ 60 | "-37.8162553", 61 | "-37.815533", 62 | "144.9640149", 63 | "144.9665099" 64 | ], 65 | "lat":"-37.8157915", 66 | "lon":"144.9656171", 67 | "display_name":"Collins Street, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia", 68 | "class":"highway", 69 | "type":"tertiary", 70 | "importance":0.51 71 | } 72 | ]` 73 | response2 = `{ 74 | "place_id":"5122082", 75 | "licence":"Data © OpenStreetMap contributors, ODbL 1.0. http:\/\/www.openstreetmap.org\/copyright", 76 | "osm_type":"node", 77 | "osm_id":"594206614", 78 | "lat":"-37.8158091", 79 | "lon":"144.9656492", 80 | "display_name":"Telstra, Collins Street, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia", 81 | "address":{ 82 | "telephone":"Telstra", 83 | "road":"Collins Street", 84 | "suburb":"Melbourne", 85 | "city":"Melbourne", 86 | "county":"City of Melbourne", 87 | "region":"Greater Melbourne", 88 | "state":"Victoria", 89 | "postcode":"3000", 90 | "country":"Australia", 91 | "country_code":"au" 92 | }, 93 | "boundingbox":[ 94 | "-37.8159091", 95 | "-37.8157091", 96 | "144.9655492", 97 | "144.9657492" 98 | ] 99 | }` 100 | response3 = `{ 101 | "error":"Unable to geocode" 102 | }` 103 | ) 104 | -------------------------------------------------------------------------------- /arcgis/geocoder_test.go: -------------------------------------------------------------------------------- 1 | package arcgis 2 | 3 | import ( 4 | "math" 5 | "net/http" 6 | "net/http/httptest" 7 | "os" 8 | "testing" 9 | 10 | geo "github.com/codingsince1985/geo-golang" 11 | ) 12 | 13 | var token = os.Getenv("ARCGIS_TOKEN") 14 | 15 | func TestGeocode(t *testing.T) { 16 | ts := testServer(geocodeResp) 17 | defer ts.Close() 18 | 19 | address := "380 New York, Redlands, CA 92373, USA" 20 | geocoder := Geocoder(token, ts.URL) 21 | loc, err := geocoder.Geocode(address) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | if loc == nil { 27 | t.Fatalf("Address: %s - Not Found\n", address) 28 | } 29 | expected := geo.Location{Lng: -117.1956703176181, Lat: 34.056488119308924} 30 | if math.Abs(loc.Lng-expected.Lng) > eps { 31 | t.Fatalf("Got: %v\tExpected: %v\n", loc, expected) 32 | } 33 | if math.Abs(loc.Lat-expected.Lat) > eps { 34 | t.Fatalf("Got: %v\tExpected: %v\n", loc, expected) 35 | } 36 | } 37 | 38 | func TestReverseGeocode(t *testing.T) { 39 | ts := testServer(reverseResp) 40 | defer ts.Close() 41 | 42 | code := "USA" 43 | state := "California" 44 | lat := 34.056488119308924 45 | lng := -117.1956703176181 46 | geocoder := Geocoder(token, ts.URL) 47 | addr, err := geocoder.ReverseGeocode(lat, lng) 48 | if err != nil { 49 | t.Error(err) 50 | } 51 | 52 | if addr == nil { 53 | t.Fatalf("Location: lat:%f, lng:%f - Not Found\n", lat, lng) 54 | } 55 | 56 | if addr.CountryCode != code { 57 | t.Fatalf("Got: %v\tExpected: %v\n", addr.CountryCode, code) 58 | } 59 | if addr.State != state { 60 | t.Fatalf("Got: %v\tExpected: %v\n", addr.State, state) 61 | } 62 | } 63 | 64 | func testServer(response string) *httptest.Server { 65 | return httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 66 | resp.Write([]byte(response)) 67 | })) 68 | } 69 | 70 | const ( 71 | eps = 1.0e-5 72 | 73 | geocodeResp = `{ 74 | "spatialReference": { 75 | "wkid": 4326, 76 | "latestWkid": 4326 77 | }, 78 | "candidates": [ 79 | { 80 | "address": "380 New York St, Redlands, California, 92373", 81 | "location": { 82 | "x": -117.1956703176181, 83 | "y": 34.056488119308924 84 | }, 85 | "score": 100, 86 | "attributes": { 87 | 88 | }, 89 | "extent": { 90 | "xmin": -117.1963135, 91 | "ymin": 34.055108000000011, 92 | "xmax": -117.19431349999999, 93 | "ymax": 34.057108000000007 94 | } 95 | } 96 | ] 97 | }` 98 | 99 | reverseResp = `{ 100 | "address": { 101 | "Match_addr": "117 Norwood St, Redlands, California, 92373", 102 | "LongLabel": "117 Norwood St, Redlands, CA, 92373, USA", 103 | "ShortLabel": "117 Norwood St", 104 | "Addr_type": "PointAddress", 105 | "Type": "", 106 | "PlaceName": "", 107 | "AddNum": "117", 108 | "Address": "117 Norwood St", 109 | "Block": "", 110 | "Sector": "", 111 | "Neighborhood": "South Redlands", 112 | "District": "", 113 | "City": "Redlands", 114 | "MetroArea": "Inland Empire", 115 | "Subregion": "San Bernardino", 116 | "Region": "California", 117 | "Territory": "", 118 | "Postal": "92373", 119 | "PostalExt": "", 120 | "CountryCode": "USA" 121 | }, 122 | "location": { 123 | "x": -117.19052869813629, 124 | "y": 34.049723195135194, 125 | "spatialReference": { 126 | "wkid": 4326, 127 | "latestWkid": 4326 128 | } 129 | } 130 | }` 131 | ) 132 | -------------------------------------------------------------------------------- /mapbox/geocoder.go: -------------------------------------------------------------------------------- 1 | // Package mapbox is a geo-golang based Mapbox geocode/reverse geocode client 2 | package mapbox 3 | 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/codingsince1985/geo-golang" 10 | ) 11 | 12 | type ( 13 | baseURL string 14 | geocodeResponse struct { 15 | Features []struct { 16 | PlaceName string `json:"place_name"` 17 | Center [2]float64 18 | Text string `json:"text"` // usually street name 19 | Address json.RawMessage `json:"address"` // potentially house number 20 | Context []struct { 21 | Text string `json:"text"` 22 | Id string `json:"id"` 23 | ShortCode string `json:"short_code"` 24 | Wikidata string `json:"wikidata"` 25 | } 26 | } 27 | Message string `json:"message"` 28 | } 29 | ) 30 | 31 | const ( 32 | mapboxPrefixLocality = "place" 33 | mapboxPrefixPostcode = "postcode" 34 | mapboxPrefixState = "region" 35 | mapboxPrefixCountry = "country" 36 | ) 37 | 38 | // Geocoder constructs Mapbox geocoder 39 | func Geocoder(token string, baseURLs ...string) geo.Geocoder { 40 | return geo.HTTPGeocoder{ 41 | EndpointBuilder: baseURL(getURL(token, baseURLs...)), 42 | ResponseParserFactory: func() geo.ResponseParser { return &geocodeResponse{} }, 43 | } 44 | } 45 | 46 | func getURL(token string, baseURLs ...string) string { 47 | if len(baseURLs) > 0 { 48 | return baseURLs[0] 49 | } 50 | return "https://api.mapbox.com/geocoding/v5/mapbox.places/*.json?limit=1&access_token=" + token 51 | } 52 | 53 | func (b baseURL) GeocodeURL(address string) string { 54 | return strings.Replace(string(b), "*", address, 1) 55 | } 56 | 57 | func (b baseURL) ReverseGeocodeURL(l geo.Location) string { 58 | return strings.Replace(string(b), "*", fmt.Sprintf("%+f,%+f", l.Lng, l.Lat), 1) 59 | } 60 | 61 | func (r *geocodeResponse) Location() (*geo.Location, error) { 62 | if len(r.Features) == 0 { 63 | // error in response 64 | if r.Message != "" { 65 | return nil, fmt.Errorf("reverse geocoding error: %s", r.Message) 66 | } 67 | // no results 68 | return nil, nil 69 | } 70 | 71 | g := r.Features[0] 72 | return &geo.Location{ 73 | Lat: g.Center[1], 74 | Lng: g.Center[0], 75 | }, nil 76 | } 77 | 78 | func (r *geocodeResponse) Address() (*geo.Address, error) { 79 | if len(r.Features) == 0 { 80 | // error in response 81 | if r.Message != "" { 82 | return nil, fmt.Errorf("reverse geocoding error: %s", r.Message) 83 | } 84 | // no results 85 | return nil, nil 86 | } 87 | 88 | return parseMapboxResponse(r), nil 89 | } 90 | 91 | func parseMapboxResponse(r *geocodeResponse) *geo.Address { 92 | addr := &geo.Address{} 93 | f := r.Features[0] 94 | addr.FormattedAddress = f.PlaceName 95 | addr.Street = f.Text 96 | addr.HouseNumber = string(f.Address) 97 | for _, c := range f.Context { 98 | if strings.HasPrefix(c.Id, mapboxPrefixLocality) { 99 | addr.City = c.Text 100 | } else if strings.HasPrefix(c.Id, mapboxPrefixPostcode) { 101 | addr.Postcode = c.Text 102 | } else if strings.HasPrefix(c.Id, mapboxPrefixState) { 103 | addr.State = c.Text 104 | addr.StateCode = c.ShortCode 105 | } else if strings.HasPrefix(c.Id, mapboxPrefixCountry) { 106 | addr.Country = c.Text 107 | addr.CountryCode = strings.ToUpper(c.ShortCode) 108 | } 109 | } 110 | 111 | return addr 112 | } 113 | -------------------------------------------------------------------------------- /here/geocoder.go: -------------------------------------------------------------------------------- 1 | // Package here is a geo-golang based HERE geocode/reverse geocode client for the legacy geocoder API 2 | package here 3 | 4 | import ( 5 | "fmt" 6 | 7 | "github.com/codingsince1985/geo-golang" 8 | ) 9 | 10 | type ( 11 | baseURL struct{ forGeocode, forReverseGeocode string } 12 | geocodeResponse struct { 13 | Response struct { 14 | View []struct { 15 | Result []struct { 16 | Location struct { 17 | DisplayPosition struct { 18 | Latitude, Longitude float64 19 | } 20 | Address struct { 21 | Label string 22 | Country string 23 | State string 24 | County string 25 | City string 26 | District string 27 | Street string 28 | HouseNumber string 29 | PostalCode string 30 | AdditionalData []struct { 31 | Key string 32 | Value string 33 | } 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | ) 41 | 42 | // Key*Name represents constants for geocoding more address detail 43 | const ( 44 | KeyCountryName = "CountryName" 45 | KeyStateName = "StateName" 46 | KeyCountyName = "CountyName" 47 | ) 48 | 49 | var r = 100 50 | 51 | // Geocoder constructs HERE geocoder 52 | func Geocoder(id, code string, radius int, baseURLs ...string) geo.Geocoder { 53 | if radius > 0 { 54 | r = radius 55 | } 56 | p := "gen=9&app_id=" + id + "&app_code=" + code 57 | return geo.HTTPGeocoder{ 58 | EndpointBuilder: baseURL{ 59 | getGeocodeURL(p, baseURLs...), 60 | getReverseGeocodeURL(p, baseURLs...)}, 61 | ResponseParserFactory: func() geo.ResponseParser { return &geocodeResponse{} }, 62 | } 63 | } 64 | 65 | func getGeocodeURL(p string, baseURLs ...string) string { 66 | if len(baseURLs) > 0 { 67 | return baseURLs[0] 68 | } 69 | return "http://geocoder.api.here.com/6.2/geocode.json?" + p 70 | } 71 | 72 | func getReverseGeocodeURL(p string, baseURLs ...string) string { 73 | if len(baseURLs) > 0 { 74 | return baseURLs[0] 75 | } 76 | return "http://reverse.geocoder.api.here.com/6.2/reversegeocode.json?mode=retrieveAddresses&" + p 77 | } 78 | 79 | func (b baseURL) GeocodeURL(address string) string { return b.forGeocode + "&searchtext=" + address } 80 | 81 | func (b baseURL) ReverseGeocodeURL(l geo.Location) string { 82 | return b.forReverseGeocode + fmt.Sprintf("&prox=%f,%f,%d", l.Lat, l.Lng, r) 83 | } 84 | 85 | func (r *geocodeResponse) Location() (*geo.Location, error) { 86 | if len(r.Response.View) == 0 { 87 | return nil, nil 88 | } 89 | p := r.Response.View[0].Result[0].Location.DisplayPosition 90 | return &geo.Location{ 91 | Lat: p.Latitude, 92 | Lng: p.Longitude, 93 | }, nil 94 | } 95 | 96 | func (r *geocodeResponse) Address() (*geo.Address, error) { 97 | if len(r.Response.View) == 0 || len(r.Response.View[0].Result) == 0 { 98 | return nil, nil 99 | } 100 | 101 | res := r.Response.View[0].Result[0].Location.Address 102 | 103 | addr := &geo.Address{ 104 | FormattedAddress: res.Label, 105 | Street: res.Street, 106 | HouseNumber: res.HouseNumber, 107 | City: res.City, 108 | Postcode: res.PostalCode, 109 | CountryCode: res.Country, 110 | } 111 | for _, v := range res.AdditionalData { 112 | switch v.Key { 113 | case KeyCountryName: 114 | addr.Country = v.Value 115 | case KeyStateName: 116 | addr.State = v.Value 117 | } 118 | } 119 | return addr, nil 120 | } 121 | -------------------------------------------------------------------------------- /frenchapigouv/geocoder.go: -------------------------------------------------------------------------------- 1 | // Package frenchapigouv is a geo-golang based French API Gouv geocode/reverse geocode client 2 | package frenchapigouv 3 | 4 | import ( 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/codingsince1985/geo-golang" 9 | ) 10 | 11 | type ( 12 | baseURL string 13 | geocodeResponse struct { 14 | Type string 15 | Version string 16 | Features []struct { 17 | Type string 18 | Geometry struct { 19 | Type string 20 | Coordinates []float64 21 | } 22 | Properties struct { 23 | Label string 24 | Score float64 25 | Housenumber string 26 | Citycode string 27 | Context string 28 | Postcode string 29 | Name string 30 | ID string 31 | Y float64 32 | Importance float64 33 | Type string 34 | City string 35 | X float64 36 | Street string 37 | } 38 | } 39 | Attribution string 40 | Licence string 41 | Query string 42 | Limit int 43 | } 44 | context struct { 45 | state string 46 | county string 47 | countyCode string 48 | } 49 | ) 50 | 51 | // Geocoder constructs FrenchApiGouv geocoder 52 | func Geocoder() geo.Geocoder { return GeocoderWithURL("https://api-adresse.data.gouv.fr/") } 53 | 54 | // GeocoderWithURL constructs French API Gouv geocoder using a custom installation of Nominatim 55 | func GeocoderWithURL(url string) geo.Geocoder { 56 | return geo.HTTPGeocoder{ 57 | EndpointBuilder: baseURL(url), 58 | ResponseParserFactory: func() geo.ResponseParser { return &geocodeResponse{} }, 59 | } 60 | } 61 | 62 | func (b baseURL) GeocodeURL(address string) string { 63 | return string(b) + "search?limit=1&q=" + address 64 | } 65 | 66 | func (b baseURL) ReverseGeocodeURL(l geo.Location) string { 67 | return string(b) + "reverse?" + fmt.Sprintf("lat=%f&lon=%f", l.Lat, l.Lng) 68 | } 69 | 70 | func (r *geocodeResponse) Location() (*geo.Location, error) { 71 | if len(r.Features) == 0 || len(r.Features[0].Geometry.Coordinates) < 2 { 72 | return nil, nil 73 | } 74 | p := r.Features[0].Geometry.Coordinates 75 | return &geo.Location{ 76 | Lat: p[1], 77 | Lng: p[0], 78 | }, nil 79 | } 80 | 81 | func (r *geocodeResponse) Address() (*geo.Address, error) { 82 | if len(r.Features) == 0 || r.Features[0].Properties.Label == "baninfo" { 83 | return nil, nil 84 | } 85 | p := r.Features[0].Properties 86 | c := r.parseContext() 87 | 88 | if p.Type == "street" || p.Type == "locality" { 89 | p.Street = p.Name 90 | } 91 | return &geo.Address{ 92 | FormattedAddress: strings.Join(strings.Fields(strings.Trim(fmt.Sprintf("%s, %s, %s, %s, %s, %s, %s", p.Housenumber, p.Street, p.Postcode, p.City, c.county, c.state, "France"), " ,")), " "), 93 | HouseNumber: p.Housenumber, 94 | Street: p.Street, 95 | Postcode: p.Postcode, 96 | City: p.City, 97 | State: c.state, 98 | County: c.county, 99 | Country: "France", 100 | CountryCode: "FRA", 101 | }, nil 102 | } 103 | 104 | func (r *geocodeResponse) parseContext() *context { 105 | var c context 106 | if len(r.Features) > 0 { 107 | p := r.Features[0].Properties 108 | f := strings.Split(p.Context, ",") 109 | for i := range f { 110 | switch i { 111 | case 0: 112 | c.countyCode = f[i] 113 | case 1: 114 | c.county = strings.TrimSpace(f[i]) 115 | case 2: 116 | c.state = strings.TrimSpace(f[i]) 117 | } 118 | } 119 | } 120 | return &c 121 | } 122 | -------------------------------------------------------------------------------- /openstreetmap/geocoder_test.go: -------------------------------------------------------------------------------- 1 | package openstreetmap_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/codingsince1985/geo-golang" 10 | "github.com/codingsince1985/geo-golang/openstreetmap" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestGeocode(t *testing.T) { 15 | ts := testServer(response1) 16 | defer ts.Close() 17 | 18 | geocoder := openstreetmap.GeocoderWithURL(ts.URL + "/") 19 | location, err := geocoder.Geocode("60 Collins St, Melbourne VIC 3000") 20 | assert.Nil(t, err) 21 | assert.Equal(t, geo.Location{Lat: -37.8157915, Lng: 144.9656171}, *location) 22 | } 23 | 24 | func TestReverseGeocode(t *testing.T) { 25 | ts := testServer(response2) 26 | defer ts.Close() 27 | 28 | geocoder := openstreetmap.GeocoderWithURL(ts.URL + "/") 29 | address, err := geocoder.ReverseGeocode(-37.8157915, 144.9656171) 30 | assert.Nil(t, err) 31 | assert.True(t, strings.Index(address.FormattedAddress, "Collins St") > 0) 32 | } 33 | 34 | func TestReverseGeocodeWithNoResult(t *testing.T) { 35 | ts := testServer(response3) 36 | defer ts.Close() 37 | 38 | geocoder := openstreetmap.GeocoderWithURL(ts.URL + "/") 39 | //geocoder := openstreetmap.Geocoder() 40 | addr, err := geocoder.ReverseGeocode(-37.8157915, 164.9656171) 41 | assert.Nil(t, addr) 42 | assert.NotNil(t, err) 43 | } 44 | 45 | func TestReverseGeocodeWithBrokenResponse(t *testing.T) { 46 | ts := testServer(response4) 47 | defer ts.Close() 48 | 49 | geocoder := openstreetmap.GeocoderWithURL(ts.URL + "/") 50 | addr, err := geocoder.ReverseGeocode(-37.8157915, 164.9656171) 51 | assert.Nil(t, addr) 52 | assert.NotNil(t, err) 53 | } 54 | 55 | func testServer(response string) *httptest.Server { 56 | return httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 57 | resp.Write([]byte(response)) 58 | })) 59 | } 60 | 61 | const ( 62 | response1 = `[ 63 | { 64 | "place_id":"133372311", 65 | "licence":"Data © OpenStreetMap contributors, ODbL 1.0. http:\/\/www.openstreetmap.org\/copyright", 66 | "osm_type":"way", 67 | "osm_id":"316166613", 68 | "boundingbox":[ 69 | "-37.8162553", 70 | "-37.815533", 71 | "144.9640149", 72 | "144.9665099" 73 | ], 74 | "lat":"-37.8157915", 75 | "lon":"144.9656171", 76 | "display_name":"Collins Street, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia", 77 | "class":"highway", 78 | "type":"tertiary", 79 | "importance":0.51 80 | } 81 | ]` 82 | response2 = `{ 83 | "place_id":"5122082", 84 | "licence":"Data © OpenStreetMap contributors, ODbL 1.0. http:\/\/www.openstreetmap.org\/copyright", 85 | "osm_type":"node", 86 | "osm_id":"594206614", 87 | "lat":"-37.8158091", 88 | "lon":"144.9656492", 89 | "display_name":"Telstra, Collins Street, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia", 90 | "address":{ 91 | "telephone":"Telstra", 92 | "road":"Collins Street", 93 | "suburb":"Melbourne", 94 | "city":"Melbourne", 95 | "county":"City of Melbourne", 96 | "region":"Greater Melbourne", 97 | "state":"Victoria", 98 | "postcode":"3000", 99 | "country":"Australia", 100 | "country_code":"au" 101 | }, 102 | "boundingbox":[ 103 | "-37.8159091", 104 | "-37.8157091", 105 | "144.9655492", 106 | "144.9657492" 107 | ] 108 | }` 109 | response3 = `{ 110 | "error":"Unable to geocode" 111 | }` 112 | response4 = `{ 113 | broken response 114 | }` 115 | ) 116 | -------------------------------------------------------------------------------- /amap/amap_test.go: -------------------------------------------------------------------------------- 1 | package amap_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "os" 7 | "testing" 8 | 9 | "github.com/codingsince1985/geo-golang" 10 | "github.com/codingsince1985/geo-golang/amap" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | var key = os.Getenv("AMAP_APP_KEY") 15 | 16 | func TestGeocode(t *testing.T) { 17 | ts := testServer(response1) 18 | defer ts.Close() 19 | 20 | geocoder := amap.Geocoder(key, 1000, ts.URL+"/") 21 | location, err := geocoder.Geocode("北京市海淀区清河街道西二旗西路领秀新硅谷") 22 | 23 | assert.NoError(t, err) 24 | assert.NotNil(t, location) 25 | if location != nil { 26 | assert.Equal(t, geo.Location{Lat: 40.055106, Lng: 116.309866}, *location) 27 | } 28 | } 29 | 30 | func TestReverseGeocode(t *testing.T) { 31 | ts := testServer(response2) 32 | defer ts.Close() 33 | 34 | geocoder := amap.Geocoder(key, 1000, ts.URL+"/") 35 | address, err := geocoder.ReverseGeocode(116.3084202915042, 116.3084202915042) 36 | 37 | assert.NoError(t, err) 38 | assert.Equal(t, address.FormattedAddress, "北京市海淀区上地街道树村郊野公园") 39 | } 40 | 41 | func TestReverseGeocodeWithNoResult(t *testing.T) { 42 | ts := testServer(response3) 43 | defer ts.Close() 44 | 45 | geocoder := amap.Geocoder(key, 1000, ts.URL+"/") 46 | addr, err := geocoder.ReverseGeocode(-37.81375, 164.97176) 47 | 48 | assert.NoError(t, err) 49 | assert.Nil(t, addr) 50 | } 51 | 52 | func testServer(response string) *httptest.Server { 53 | return httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 54 | resp.Write([]byte(response)) 55 | })) 56 | } 57 | 58 | const ( 59 | response1 = `1OK100001北京市海淀区清河街道西二旗西路领秀新硅谷中国北京市010北京市海淀区110108西二旗西路116.309866,40.055106住宅区` 60 | response2 = `1OK10000北京市海淀区上地街道树村郊野公园中国北京市010海淀区110108上地街道110108022000树村郊野公园风景名胜;公园广场;公园马连洼北路29号116.299587,40.034620147.306116.303276,40.035542上地110108116.256057,40.054273西北 110108116.281156,40.028654马连洼110108` 61 | response3 = `1OK10000` 62 | ) 63 | -------------------------------------------------------------------------------- /locationiq/geocoder_test.go: -------------------------------------------------------------------------------- 1 | package locationiq 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestGeocodeYieldsResult(t *testing.T) { 11 | ts := testServer(responseForGeocode) 12 | defer ts.Close() 13 | 14 | gc := Geocoder("foobar", 18, ts.URL+"/") 15 | l, err := gc.Geocode("Seidlstraße 26, 80335 München") 16 | 17 | expLat := 48.1453641 18 | expLon := 11.5582083 19 | 20 | if err != nil { 21 | t.Errorf("Expected nil error, got %v", err) 22 | } 23 | 24 | if expLat != l.Lat { 25 | t.Errorf("Expected latitude: %f, got %f", expLat, l.Lat) 26 | } 27 | 28 | if expLon != l.Lng { 29 | t.Errorf("Expected longitude %f, got %f", expLon, l.Lng) 30 | } 31 | } 32 | 33 | func TestGeocodeYieldsNoResult(t *testing.T) { 34 | ts := testServer("[]") 35 | defer ts.Close() 36 | 37 | gc := Geocoder("foobar", 18, ts.URL+"/") 38 | l, err := gc.Geocode("Seidlstraße 26, 80335 München") 39 | 40 | if l != nil { 41 | t.Errorf("Expected nil, got %#v", l) 42 | } 43 | 44 | if err != nil { 45 | t.Errorf("Expected nil error, got %v", err) 46 | } 47 | } 48 | 49 | func TestReverseGeocodeYieldsResult(t *testing.T) { 50 | ts := testServer(responseForReverse) 51 | defer ts.Close() 52 | 53 | gc := Geocoder("foobar", 18, ts.URL+"/") 54 | addr, err := gc.ReverseGeocode(48.1453641, 11.5582083) 55 | 56 | if err != nil { 57 | t.Errorf("Expected nil error, got %v", err) 58 | } 59 | if !strings.HasPrefix(addr.FormattedAddress, "26, Seidlstraße") { 60 | t.Errorf("Expected address string starting with %s, got string: %s", "26, Seidlstraße", addr) 61 | } 62 | } 63 | 64 | func TestReverseGeocodeYieldsNoResult(t *testing.T) { 65 | ts := testServer(errorResponse) 66 | defer ts.Close() 67 | 68 | gc := Geocoder("foobar", 18, ts.URL+"/") 69 | addr, err := gc.ReverseGeocode(48.1453641, 11.5582083) 70 | 71 | if err == nil { 72 | t.Error("Expected error, got nil") 73 | } 74 | if addr != nil { 75 | t.Errorf("Expected nil as address, got: %s", addr) 76 | } 77 | } 78 | 79 | func testServer(response string) *httptest.Server { 80 | return httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 81 | resp.Write([]byte(response)) 82 | })) 83 | } 84 | 85 | const ( 86 | responseForGeocode = `[ 87 | { 88 | "place_id": "25798174", 89 | "licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://www.openstreetmap.org/copyright", 90 | "osm_type": "node", 91 | "osm_id": "2475749822", 92 | "boundingbox": [ 93 | "48.1453141", 94 | "48.1454141", 95 | "11.5581583", 96 | "11.5582583" 97 | ], 98 | "lat": "48.1453641", 99 | "lon": "11.5582083", 100 | "display_name": "26, Seidlstraße, Bezirksteil Augustenstraße, Maxvorstadt, Munich, Upper Bavaria, Free State of Bavaria, 80335, Germany", 101 | "class": "place", 102 | "type": "house", 103 | "importance": 0.111 104 | } 105 | ]` 106 | responseForReverse = `{ 107 | "place_id": "25798174", 108 | "licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://www.openstreetmap.org/copyright", 109 | "osm_type": "node", 110 | "osm_id": "2475749822", 111 | "lat": "48.1453641", 112 | "lon": "11.5582083", 113 | "display_name": "26, Seidlstraße, Bezirksteil Augustenstraße, Maxvorstadt, Munich, Upper Bavaria, Free State of Bavaria, 80335, Germany", 114 | "address": { 115 | "house_number": "26", 116 | "road": "Seidlstraße", 117 | "suburb": "Maxvorstadt", 118 | "city": "Munich", 119 | "state_district": "Upper Bavaria", 120 | "state": "Free State of Bavaria", 121 | "postcode": "80335", 122 | "country": "Germany", 123 | "country_code": "de" 124 | }, 125 | "boundingbox": [ 126 | "48.1452641", 127 | "48.1454641", 128 | "11.5581083", 129 | "11.5583083" 130 | ] 131 | }` 132 | errorResponse = `{ 133 | "error": "Unable to geocode" 134 | }` 135 | ) 136 | -------------------------------------------------------------------------------- /google/geocoder.go: -------------------------------------------------------------------------------- 1 | // Package google is a geo-golang based Google Geo Location API 2 | // https://developers.google.com/maps/documentation/geocoding/intro 3 | package google 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/codingsince1985/geo-golang" 9 | ) 10 | 11 | type ( 12 | baseURL string 13 | geocodeResponse struct { 14 | Results []struct { 15 | FormattedAddress string `json:"formatted_address"` 16 | AddressComponents []googleAddressComponent `json:"address_components"` 17 | Geometry struct { 18 | Location geo.Location 19 | } 20 | } 21 | Status string `json:"status"` 22 | } 23 | googleAddressComponent struct { 24 | LongName string `json:"long_name"` 25 | ShortName string `json:"short_name"` 26 | Types []string `json:"types"` 27 | } 28 | ) 29 | 30 | const ( 31 | statusOK = "OK" 32 | statusNoResults = "ZERO_RESULTS" 33 | componentTypeHouseNumber = "street_number" 34 | componentTypeStreetName = "route" 35 | componentTypeSuburb = "sublocality" 36 | componentTypeLocality = "locality" 37 | componentTypeStateDistrict = "administrative_area_level_2" 38 | componentTypeState = "administrative_area_level_1" 39 | componentTypeCountry = "country" 40 | componentTypePostcode = "postal_code" 41 | ) 42 | 43 | // Geocoder constructs Google geocoder 44 | func Geocoder(apiKey string, baseURLs ...string) geo.Geocoder { 45 | return geo.HTTPGeocoder{ 46 | EndpointBuilder: baseURL(getURL(apiKey, baseURLs...)), 47 | ResponseParserFactory: func() geo.ResponseParser { return &geocodeResponse{} }, 48 | } 49 | } 50 | 51 | func getURL(apiKey string, baseURLs ...string) string { 52 | if len(baseURLs) > 0 { 53 | return baseURLs[0] 54 | } 55 | return fmt.Sprintf("https://maps.googleapis.com/maps/api/geocode/json?key=%s&", apiKey) 56 | } 57 | 58 | func (b baseURL) GeocodeURL(address string) string { return string(b) + "address=" + address } 59 | 60 | func (b baseURL) ReverseGeocodeURL(l geo.Location) string { 61 | return string(b) + fmt.Sprintf("result_type=street_address&latlng=%f,%f", l.Lat, l.Lng) 62 | } 63 | 64 | func (r *geocodeResponse) Location() (*geo.Location, error) { 65 | if r.Status == statusNoResults { 66 | return nil, nil 67 | } else if r.Status != statusOK { 68 | return nil, fmt.Errorf("geocoding error: %s", r.Status) 69 | } 70 | 71 | return &r.Results[0].Geometry.Location, nil 72 | } 73 | 74 | func (r *geocodeResponse) Address() (*geo.Address, error) { 75 | if r.Status == statusNoResults { 76 | return nil, nil 77 | } else if r.Status != statusOK { 78 | return nil, fmt.Errorf("reverse geocoding error: %s", r.Status) 79 | } 80 | 81 | if len(r.Results) == 0 || len(r.Results[0].AddressComponents) == 0 { 82 | return nil, nil 83 | } 84 | 85 | addr := parseGoogleResult(r) 86 | 87 | return addr, nil 88 | } 89 | 90 | func parseGoogleResult(r *geocodeResponse) *geo.Address { 91 | addr := &geo.Address{} 92 | res := r.Results[0] 93 | addr.FormattedAddress = res.FormattedAddress 94 | OuterLoop: 95 | for _, comp := range res.AddressComponents { 96 | for _, typ := range comp.Types { 97 | switch typ { 98 | case componentTypeHouseNumber: 99 | addr.HouseNumber = comp.LongName 100 | continue OuterLoop 101 | case componentTypeStreetName: 102 | addr.Street = comp.LongName 103 | continue OuterLoop 104 | case componentTypeSuburb: 105 | addr.Suburb = comp.LongName 106 | continue OuterLoop 107 | case componentTypeLocality: 108 | addr.City = comp.LongName 109 | continue OuterLoop 110 | case componentTypeStateDistrict: 111 | addr.StateDistrict = comp.LongName 112 | continue OuterLoop 113 | case componentTypeState: 114 | addr.State = comp.LongName 115 | addr.StateCode = comp.ShortName 116 | continue OuterLoop 117 | case componentTypeCountry: 118 | addr.Country = comp.LongName 119 | addr.CountryCode = comp.ShortName 120 | continue OuterLoop 121 | case componentTypePostcode: 122 | addr.Postcode = comp.LongName 123 | continue OuterLoop 124 | } 125 | } 126 | } 127 | 128 | return addr 129 | } 130 | -------------------------------------------------------------------------------- /amap/amap.go: -------------------------------------------------------------------------------- 1 | package amap 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/codingsince1985/geo-golang" 9 | ) 10 | 11 | type ( 12 | baseURL string 13 | geocodeResponse struct { 14 | XMLName xml.Name `xml:"response"` 15 | Status int `xml:"status"` 16 | Info string `xml:"info"` 17 | Infocode int `xml:"infocode"` 18 | Count int `xml:"count"` 19 | Geocodes []struct { 20 | FormattedAddress string `xml:"formatted_address"` 21 | Country string `xml:"country"` 22 | Province string `xml:"province"` 23 | Citycode string `xml:"citycode"` 24 | City string `xml:"city"` 25 | District string `xml:"district"` 26 | Adcode string `xml:"adcode"` 27 | Street string `xml:"street"` 28 | Number string `xml:"number"` 29 | Location string `xml:"location"` 30 | Level string `xml:"level"` 31 | } `xml:"geocodes>geocode"` 32 | Regeocode struct { 33 | FormattedAddress string `xml:"formatted_address"` 34 | AddressComponent struct { 35 | Country string `xml:"country"` 36 | Township string `xml:"township"` 37 | District string `xml:"district"` 38 | Adcode string `xml:"adcode"` 39 | Province string `xml:"province"` 40 | Citycode string `xml:"citycode"` 41 | StreetNumber struct { 42 | Number string `xml:"number"` 43 | Location string `xml:"location"` 44 | Direction string `xml:"direction"` 45 | Distance string `xml:"distance"` 46 | Street string `xml:"street"` 47 | } `xml:"streetNumber"` 48 | } `xml:"addressComponent"` 49 | } `xml:"regeocode"` 50 | } 51 | ) 52 | 53 | const ( 54 | statusOK = 1 55 | ) 56 | 57 | var r = 1000 58 | 59 | // Geocoder constructs AMAP geocoder 60 | func Geocoder(key string, radius int, baseURLs ...string) geo.Geocoder { 61 | if radius > 0 { 62 | r = radius 63 | } 64 | return geo.HTTPGeocoder{ 65 | EndpointBuilder: baseURL(getURL(key, baseURLs...)), 66 | ResponseParserFactory: func() geo.ResponseParser { return &geocodeResponse{} }, 67 | ResponseUnmarshaler: &geo.XMLUnmarshaler{}, 68 | } 69 | } 70 | 71 | func getURL(apiKey string, baseURLs ...string) string { 72 | if len(baseURLs) > 0 { 73 | return baseURLs[0] 74 | } 75 | return fmt.Sprintf("https://restapi.amap.com/v3/geocode/*?key=%s&", apiKey) 76 | } 77 | 78 | // GeocodeURL https://restapi.amap.com/v3/geocode/geo?&output=XML&key=APPKEY&address=ADDRESS 79 | func (b baseURL) GeocodeURL(address string) string { 80 | return strings.Replace(string(b), "*", "geo", 1) + fmt.Sprintf("output=XML&address=%s", address) 81 | } 82 | 83 | // ReverseGeocodeURL https://restapi.amap.com/v3/geocode/regeo?output=XML&key=APPKEY&radius=1000&extensions=all&location=31.225696563611,121.49884033194 84 | func (b baseURL) ReverseGeocodeURL(l geo.Location) string { 85 | return strings.Replace(string(b), "*", "regeo", 1) + fmt.Sprintf("output=XML&location=%f,%f&radius=%d&extensions=all", l.Lng, l.Lat, r) 86 | } 87 | 88 | func (r *geocodeResponse) Location() (*geo.Location, error) { 89 | var location = &geo.Location{} 90 | if len(r.Geocodes) == 0 { 91 | return nil, nil 92 | } 93 | if r.Status != statusOK { 94 | return nil, fmt.Errorf("geocoding error: %v", r.Status) 95 | } 96 | fmt.Sscanf(string(r.Geocodes[0].Location), "%f,%f", &location.Lng, &location.Lat) 97 | return location, nil 98 | } 99 | 100 | func (r *geocodeResponse) Address() (*geo.Address, error) { 101 | if r.Status != statusOK { 102 | return nil, fmt.Errorf("reverse geocoding error: %v", r.Status) 103 | } 104 | 105 | addr := parseAmapResult(r) 106 | 107 | return addr, nil 108 | } 109 | 110 | func parseAmapResult(r *geocodeResponse) *geo.Address { 111 | addr := &geo.Address{} 112 | res := r.Regeocode 113 | addr.FormattedAddress = string(res.FormattedAddress) 114 | addr.HouseNumber = string(res.AddressComponent.StreetNumber.Number) 115 | addr.Street = string(res.AddressComponent.StreetNumber.Street) 116 | addr.Suburb = string(res.AddressComponent.District) 117 | addr.State = string(res.AddressComponent.Province) 118 | addr.Country = string(res.AddressComponent.Country) 119 | 120 | if (*addr == geo.Address{}) { 121 | return nil 122 | } 123 | return addr 124 | } 125 | -------------------------------------------------------------------------------- /here/search/geocoder_test.go: -------------------------------------------------------------------------------- 1 | package search_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | 13 | "github.com/codingsince1985/geo-golang" 14 | "github.com/codingsince1985/geo-golang/here/search" 15 | ) 16 | 17 | var apiKey = os.Getenv("HERE_API_KEY") 18 | 19 | func TestGeocode(t *testing.T) { 20 | ts := testServer(response1) 21 | defer ts.Close() 22 | 23 | geocoder := search.Geocoder(apiKey, ts.URL+"/") 24 | location, err := geocoder.Geocode("60 Collins St, Melbourne VIC 3000") 25 | require.NoError(t, err, "Geocode error") 26 | require.NotNil(t, location, "Geocode location") 27 | assert.Equal(t, geo.Location{Lat: -37.81375, Lng: 144.97176}, *location) 28 | } 29 | 30 | func TestReverseGeocode(t *testing.T) { 31 | ts := testServer(response2) 32 | defer ts.Close() 33 | 34 | geocoder := search.Geocoder(apiKey, ts.URL+"/") 35 | address, err := geocoder.ReverseGeocode(-37.81375, 144.97176) 36 | require.NoError(t, err, "ReverseGeocode address") 37 | require.NotNil(t, address, "ReverseGeocode address") 38 | assert.True(t, strings.HasPrefix(address.FormattedAddress, "56-64 Collins St")) 39 | } 40 | 41 | func TestReverseGeocodeWithNoResult(t *testing.T) { 42 | ts := testServer(response3) 43 | defer ts.Close() 44 | 45 | geocoder := search.Geocoder(apiKey, ts.URL+"/") 46 | addr, _ := geocoder.ReverseGeocode(-37.81375, 164.97176) 47 | assert.Nil(t, addr) 48 | } 49 | 50 | func testServer(response string) *httptest.Server { 51 | return httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 52 | resp.Write([]byte(response)) 53 | })) 54 | } 55 | 56 | const ( 57 | response1 = `{ 58 | "items": [ 59 | { 60 | "title": "60 Collins St, Melbourne VIC 3000, Australia", 61 | "id": "here:af:streetsection:ek.gZtN5HHOffWV1FK-xUB:CggIBCDu357RAhABGgI2MChk", 62 | "resultType": "houseNumber", 63 | "houseNumberType": "PA", 64 | "address": { 65 | "label": "60 Collins St, Melbourne VIC 3000, Australia", 66 | "countryCode": "AUS", 67 | "countryName": "Australia", 68 | "stateCode": "VIC", 69 | "state": "Victoria", 70 | "city": "Melbourne", 71 | "street": "Collins St", 72 | "postalCode": "3000", 73 | "houseNumber": "60" 74 | }, 75 | "position": { 76 | "lat": -37.81375, 77 | "lng": 144.97176 78 | }, 79 | "access": [ 80 | { 81 | "lat": -37.81393, 82 | "lng": 144.97184 83 | } 84 | ], 85 | "mapView": { 86 | "west": 144.97062, 87 | "south": -37.81465, 88 | "east": 144.9729, 89 | "north": -37.81285 90 | }, 91 | "scoring": { 92 | "queryScore": 1, 93 | "fieldScore": { 94 | "state": 1, 95 | "city": 1, 96 | "streets": [ 97 | 1 98 | ], 99 | "houseNumber": 1, 100 | "postalCode": 1 101 | } 102 | } 103 | } 104 | ] 105 | }` 106 | response2 = `{ 107 | "items": [ 108 | { 109 | "title": "56-64 Collins St, Melbourne VIC 3000, Australia", 110 | "id": "here:af:streetsection:ek.gZtN5HHOffWV1FK-xUB:CggIBCChp5agARABGgU1Ni02NA", 111 | "resultType": "houseNumber", 112 | "houseNumberType": "PA", 113 | "address": { 114 | "label": "56-64 Collins St, Melbourne VIC 3000, Australia", 115 | "countryCode": "AUS", 116 | "countryName": "Australia", 117 | "stateCode": "VIC", 118 | "state": "Victoria", 119 | "city": "Melbourne", 120 | "street": "Collins St", 121 | "postalCode": "3000", 122 | "houseNumber": "56-64" 123 | }, 124 | "position": { 125 | "lat": -37.81375, 126 | "lng": 144.97176 127 | }, 128 | "access": [ 129 | { 130 | "lat": -37.81393, 131 | "lng": 144.97184 132 | } 133 | ], 134 | "distance": 0, 135 | "mapView": { 136 | "west": 144.95405, 137 | "south": -37.81909, 138 | "east": 144.97391, 139 | "north": -37.81323 140 | } 141 | } 142 | ] 143 | }` 144 | response3 = `{ 145 | "items": [] 146 | }` 147 | ) 148 | -------------------------------------------------------------------------------- /mapquest/nominatim/geocoder_test.go: -------------------------------------------------------------------------------- 1 | package nominatim_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/codingsince1985/geo-golang" 11 | "github.com/codingsince1985/geo-golang/mapquest/nominatim" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | var key = os.Getenv("MAPQUEST_NOMINATUM_KEY") 16 | 17 | func TestGeocode(t *testing.T) { 18 | ts := testServer(response1) 19 | defer ts.Close() 20 | 21 | geocoder := nominatim.Geocoder(key, ts.URL+"/") 22 | location, err := geocoder.Geocode("60 Collins St, Melbourne VIC 3000") 23 | assert.NoError(t, err) 24 | assert.Equal(t, geo.Location{Lat: -37.8137433689794, Lng: 144.971745104488}, *location) 25 | } 26 | 27 | func TestReverseGeocode(t *testing.T) { 28 | ts := testServer(response2) 29 | defer ts.Close() 30 | 31 | geocoder := nominatim.Geocoder(key, ts.URL+"/") 32 | addr, err := geocoder.ReverseGeocode(-37.8137433689794, 144.971745104488) 33 | assert.NoError(t, err) 34 | assert.True(t, strings.HasPrefix(addr.FormattedAddress, "Reserve Bank of Australia")) 35 | 36 | ts2 := testServer(response4) 37 | defer ts2.Close() 38 | 39 | geocoder = nominatim.Geocoder(key, ts2.URL+"/") 40 | addr, err = geocoder.ReverseGeocode(43.0280986, -78.8136961) 41 | assert.NoError(t, err) 42 | assert.Equal(t, "Audubon Industrial Park", addr.City) 43 | } 44 | 45 | func TestReverseGeocodeWithNoResult(t *testing.T) { 46 | ts := testServer(response3) 47 | defer ts.Close() 48 | 49 | geocoder := nominatim.Geocoder(key, ts.URL+"/") 50 | //geocoder := nominatim.Geocoder(key) 51 | addr, err := geocoder.ReverseGeocode(-37.8137433689794, 164.971745104488) 52 | assert.NotNil(t, err) 53 | assert.Nil(t, addr) 54 | } 55 | 56 | func testServer(response string) *httptest.Server { 57 | return httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 58 | resp.Write([]byte(response)) 59 | })) 60 | } 61 | 62 | const ( 63 | response1 = `[ 64 | { 65 | "place_id":"2685280165", 66 | "licence":"Data \u00a9 OpenStreetMap contributors, ODbL 1.0. http:\/\/www.openstreetmap.org\/copyright", 67 | "osm_type":"node", 68 | "osm_id":"1038589059", 69 | "boundingbox":[ 70 | "-37.8137433689794", 71 | "-37.8137433689794", 72 | "144.971745104488", 73 | "144.971745104488" 74 | ], 75 | "lat":"-37.8137433689794", 76 | "lon":"144.971745104488", 77 | "display_name":"60, Collins Street, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia", 78 | "class":"place", 79 | "type":"house", 80 | "importance":0.411 81 | } 82 | ]` 83 | response2 = `{ 84 | "place_id":"70709718", 85 | "licence":"Data \u00a9 OpenStreetMap contributors, ODbL 1.0. http:\/\/www.openstreetmap.org\/copyright", 86 | "osm_type":"way", 87 | "osm_id":"52554876", 88 | "lat":"-37.81362105", 89 | "lon":"144.971609569165", 90 | "display_name":"Reserve Bank of Australia, Exhibition Street, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia", 91 | "address":{ 92 | "building":"Reserve Bank of Australia", 93 | "road":"Exhibition Street", 94 | "suburb":"Melbourne", 95 | "city":"Melbourne", 96 | "county":"City of Melbourne", 97 | "region":"Greater Melbourne", 98 | "state":"Victoria", 99 | "postcode":"3000", 100 | "country":"Australia", 101 | "country_code":"au" 102 | } 103 | }` 104 | response3 = `{ 105 | "error":"Unable to geocode" 106 | }` 107 | response4 = `{ 108 | "place_id": 259174465, 109 | "licence": "Data \u00a9 OpenStreetMap contributors, ODbL 1.0. https:\/\/osm.org\/copyright", 110 | "osm_type": "way", 111 | "osm_id": 672030029, 112 | "lat": "43.0275119527027", 113 | "lon": "-78.81372135810811", 114 | "display_name": "188, Commerce Drive, Audubon Industrial Park, Amherst Town, Erie County, New York, 14228, United States of America", 115 | "address": { 116 | "house_number": "188", 117 | "road": "Commerce Drive", 118 | "hamlet": "Audubon Industrial Park", 119 | "county": "Erie County", 120 | "state": "New York", 121 | "postcode": "14228", 122 | "country": "United States of America", 123 | "country_code": "us" 124 | }, 125 | "boundingbox": [ 126 | "43.027411952703", 127 | "43.027611952703", 128 | "-78.813821358108", 129 | "-78.813621358108" 130 | ] 131 | }` 132 | ) 133 | -------------------------------------------------------------------------------- /http_geocoder.go: -------------------------------------------------------------------------------- 1 | package geo 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "encoding/xml" 7 | "errors" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "strconv" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | // DefaultTimeout for the request execution 17 | const DefaultTimeout = time.Second * 8 18 | 19 | // ErrTimeout occurs when no response returned within timeoutInSeconds 20 | var ErrTimeout = errors.New("TIMEOUT") 21 | 22 | // EndpointBuilder defines functions that build urls for geocode/reverse geocode 23 | type EndpointBuilder interface { 24 | GeocodeURL(string) string 25 | ReverseGeocodeURL(Location) string 26 | } 27 | 28 | // ResponseParserFactory creates a new ResponseParser 29 | type ResponseParserFactory func() ResponseParser 30 | 31 | // ResponseParser defines functions that parse response of geocode/reverse geocode 32 | type ResponseParser interface { 33 | Location() (*Location, error) 34 | Address() (*Address, error) 35 | } 36 | 37 | // HTTPGeocoder has EndpointBuilder and ResponseParser 38 | type HTTPGeocoder struct { 39 | EndpointBuilder 40 | ResponseParserFactory 41 | ResponseUnmarshaler 42 | } 43 | 44 | func (g HTTPGeocoder) geocodeWithContext(ctx context.Context, address string) (*Location, error) { 45 | responseParser := g.ResponseParserFactory() 46 | var responseUnmarshaler ResponseUnmarshaler = &JSONUnmarshaler{} 47 | if g.ResponseUnmarshaler != nil { 48 | responseUnmarshaler = g.ResponseUnmarshaler 49 | } 50 | 51 | type geoResp struct { 52 | l *Location 53 | e error 54 | } 55 | ch := make(chan geoResp, 1) 56 | 57 | go func(ch chan geoResp) { 58 | if err := response(ctx, g.GeocodeURL(url.QueryEscape(address)), responseUnmarshaler, responseParser); err != nil { 59 | ch <- geoResp{ 60 | l: nil, 61 | e: err, 62 | } 63 | } 64 | 65 | loc, err := responseParser.Location() 66 | ch <- geoResp{ 67 | l: loc, 68 | e: err, 69 | } 70 | }(ch) 71 | 72 | select { 73 | case <-ctx.Done(): 74 | return nil, ErrTimeout 75 | case res := <-ch: 76 | return res.l, res.e 77 | } 78 | } 79 | 80 | // Geocode returns location for address 81 | func (g HTTPGeocoder) Geocode(address string) (*Location, error) { 82 | ctx, cancel := context.WithTimeout(context.TODO(), DefaultTimeout) 83 | defer cancel() 84 | 85 | return g.geocodeWithContext(ctx, address) 86 | } 87 | 88 | // ReverseGeocode returns address for location 89 | func (g HTTPGeocoder) ReverseGeocode(lat, lng float64) (*Address, error) { 90 | responseParser := g.ResponseParserFactory() 91 | var responseUnmarshaler ResponseUnmarshaler = &JSONUnmarshaler{} 92 | if g.ResponseUnmarshaler != nil { 93 | responseUnmarshaler = g.ResponseUnmarshaler 94 | } 95 | 96 | ctx, cancel := context.WithTimeout(context.TODO(), DefaultTimeout) 97 | defer cancel() 98 | 99 | type revResp struct { 100 | a *Address 101 | e error 102 | } 103 | ch := make(chan revResp, 1) 104 | 105 | go func(ch chan revResp) { 106 | if err := response(ctx, g.ReverseGeocodeURL(Location{lat, lng}), responseUnmarshaler, responseParser); err != nil { 107 | ch <- revResp{ 108 | a: nil, 109 | e: err, 110 | } 111 | } 112 | 113 | addr, err := responseParser.Address() 114 | ch <- revResp{ 115 | a: addr, 116 | e: err, 117 | } 118 | }(ch) 119 | 120 | select { 121 | case <-ctx.Done(): 122 | return nil, ErrTimeout 123 | case res := <-ch: 124 | return res.a, res.e 125 | } 126 | } 127 | 128 | type ResponseUnmarshaler interface { 129 | Unmarshal(data []byte, v any) error 130 | } 131 | 132 | type JSONUnmarshaler struct{} 133 | 134 | func (*JSONUnmarshaler) Unmarshal(data []byte, v any) error { 135 | body := strings.Trim(string(data), " []") 136 | if body == "" { 137 | return nil 138 | } 139 | return json.Unmarshal([]byte(body), v) 140 | } 141 | 142 | type XMLUnmarshaler struct{} 143 | 144 | func (*XMLUnmarshaler) Unmarshal(data []byte, v any) error { 145 | return xml.Unmarshal(data, v) 146 | } 147 | 148 | // Response gets response from url 149 | func response(ctx context.Context, url string, unmarshaler ResponseUnmarshaler, obj ResponseParser) error { 150 | req, err := http.NewRequest(http.MethodGet, url, nil) 151 | if err != nil { 152 | return err 153 | } 154 | req = req.WithContext(ctx) 155 | 156 | req.Header.Add("User-Agent", "geo-golang/1.0") 157 | 158 | resp, err := http.DefaultClient.Do(req) 159 | if err != nil { 160 | return err 161 | } 162 | 163 | defer resp.Body.Close() 164 | data, err := io.ReadAll(resp.Body) 165 | if err != nil { 166 | return err 167 | } 168 | 169 | DebugLogger.Printf("Received response: %s\n", string(data)) 170 | if err := unmarshaler.Unmarshal(data, obj); err != nil { 171 | ErrLogger.Printf("Error unmarshalling response: %s\n", err.Error()) 172 | return err 173 | } 174 | 175 | return nil 176 | } 177 | 178 | // ParseFloat is a helper to parse a string to a float 179 | func ParseFloat(value string) float64 { 180 | f, _ := strconv.ParseFloat(value, 64) 181 | return f 182 | } 183 | -------------------------------------------------------------------------------- /yandex/geocoder.go: -------------------------------------------------------------------------------- 1 | // Package yandex is a geo-golang based Yandex Maps Location API 2 | package yandex 3 | 4 | import ( 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/codingsince1985/geo-golang" 10 | ) 11 | 12 | type ( 13 | baseURL string 14 | geocodeResponse struct { 15 | Response struct { 16 | GeoObjectCollection struct { 17 | MetaDataProperty struct { 18 | GeocoderResponseMetaData struct { 19 | Request string `json:"request"` 20 | Found string `json:"found"` 21 | Results string `json:"results"` 22 | } `json:"GeocoderResponseMetaData"` 23 | } `json:"metaDataProperty"` 24 | FeatureMember []*yandexFeatureMember `json:"featureMember"` 25 | } `json:"GeoObjectCollection"` 26 | } `json:"response"` 27 | } 28 | 29 | yandexFeatureMember struct { 30 | GeoObject struct { 31 | MetaDataProperty struct { 32 | GeocoderMetaData struct { 33 | Kind string `json:"kind"` 34 | Text string `json:"text"` 35 | Precision string `json:"precision"` 36 | Address struct { 37 | CountryCode string `json:"country_code"` 38 | PostalCode string `json:"postal_code"` 39 | Formatted string `json:"formatted"` 40 | Components []struct { 41 | Kind string `json:"kind"` 42 | Name string `json:"name"` 43 | } `json:"Components"` 44 | } `json:"Address"` 45 | } `json:"GeocoderMetaData"` 46 | } `json:"metaDataProperty"` 47 | Description string `json:"description"` 48 | Name string `json:"name"` 49 | BoundedBy struct { 50 | Envelope struct { 51 | LowerCorner string `json:"lowerCorner"` 52 | UpperCorner string `json:"upperCorner"` 53 | } `json:"Envelope"` 54 | } `json:"boundedBy"` 55 | Point struct { 56 | Pos string `json:"pos"` 57 | } `json:"Point"` 58 | } `json:"GeoObject"` 59 | } 60 | ) 61 | 62 | const ( 63 | componentTypeHouseNumber = "house" 64 | componentTypeStreetName = "street" 65 | componentTypeLocality = "locality" 66 | componentTypeStateDistrict = "area" 67 | componentTypeState = "province" 68 | componentTypeCountry = "country" 69 | ) 70 | 71 | // Geocoder constructs Yandex geocoder 72 | func Geocoder(apiKey string, baseURLs ...string) geo.Geocoder { 73 | return geo.HTTPGeocoder{ 74 | EndpointBuilder: baseURL(getURL(apiKey, baseURLs...)), 75 | ResponseParserFactory: func() geo.ResponseParser { return &geocodeResponse{} }, 76 | } 77 | } 78 | 79 | func getURL(apiKey string, baseURLs ...string) string { 80 | if len(baseURLs) > 0 { 81 | return baseURLs[0] 82 | } 83 | return fmt.Sprintf("https://geocode-maps.yandex.ru/1.x/?results=1&lang=en_US&format=json&apikey=%s&", apiKey) 84 | } 85 | 86 | func (b baseURL) GeocodeURL(address string) string { 87 | return string(b) + "geocode=" + address 88 | } 89 | 90 | func (b baseURL) ReverseGeocodeURL(l geo.Location) string { 91 | return string(b) + fmt.Sprintf("sco=latlong&geocode=%f,%f", l.Lat, l.Lng) 92 | } 93 | 94 | func (r *geocodeResponse) Location() (*geo.Location, error) { 95 | if r.Response.GeoObjectCollection.MetaDataProperty.GeocoderResponseMetaData.Found == "0" { 96 | return nil, nil 97 | } 98 | if len(r.Response.GeoObjectCollection.FeatureMember) == 0 { 99 | return nil, nil 100 | } 101 | featureMember := r.Response.GeoObjectCollection.FeatureMember[0] 102 | result := &geo.Location{} 103 | latLng := strings.Split(featureMember.GeoObject.Point.Pos, " ") 104 | if len(latLng) > 1 { 105 | // Yandex return geo coord in format "long lat" 106 | result.Lat, _ = strconv.ParseFloat(latLng[1], 64) 107 | result.Lng, _ = strconv.ParseFloat(latLng[0], 64) 108 | } 109 | 110 | return result, nil 111 | } 112 | 113 | func (r *geocodeResponse) Address() (*geo.Address, error) { 114 | if r.Response.GeoObjectCollection.MetaDataProperty.GeocoderResponseMetaData.Found == "0" { 115 | return nil, nil 116 | } 117 | if len(r.Response.GeoObjectCollection.FeatureMember) == 0 { 118 | return nil, nil 119 | } 120 | 121 | return parseYandexResult(r.Response.GeoObjectCollection.FeatureMember[0]), nil 122 | } 123 | 124 | func parseYandexResult(r *yandexFeatureMember) *geo.Address { 125 | addr := &geo.Address{} 126 | res := r.GeoObject.MetaDataProperty.GeocoderMetaData 127 | 128 | for _, comp := range res.Address.Components { 129 | switch comp.Kind { 130 | case componentTypeHouseNumber: 131 | addr.HouseNumber = comp.Name 132 | continue 133 | case componentTypeStreetName: 134 | addr.Street = comp.Name 135 | continue 136 | case componentTypeLocality: 137 | addr.City = comp.Name 138 | continue 139 | case componentTypeStateDistrict: 140 | addr.StateDistrict = comp.Name 141 | continue 142 | case componentTypeState: 143 | addr.State = comp.Name 144 | continue 145 | case componentTypeCountry: 146 | addr.Country = comp.Name 147 | continue 148 | } 149 | } 150 | 151 | addr.Postcode = res.Address.PostalCode 152 | addr.CountryCode = res.Address.CountryCode 153 | addr.FormattedAddress = res.Address.Formatted 154 | 155 | return addr 156 | } 157 | -------------------------------------------------------------------------------- /tomtom/geocoder_test.go: -------------------------------------------------------------------------------- 1 | package tomtom 2 | 3 | import ( 4 | "math" 5 | "net/http" 6 | "net/http/httptest" 7 | "os" 8 | "testing" 9 | 10 | geo "github.com/codingsince1985/geo-golang" 11 | ) 12 | 13 | var key = os.Getenv("TOMTOM_API_KEY") 14 | 15 | func TestGeocode(t *testing.T) { 16 | ts := testServer(geocodeResp) 17 | defer ts.Close() 18 | 19 | address := "1109 N Highland St, Arlington VA, US" 20 | geocoder := Geocoder(key, ts.URL) 21 | loc, err := geocoder.Geocode(address) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | if loc == nil { 27 | t.Fatalf("Address: %s - Not Found\n", address) 28 | } 29 | expected := geo.Location{Lng: -77.09464, Lat: 38.88669} 30 | if math.Abs(loc.Lng-expected.Lng) > eps { 31 | t.Fatalf("Got: %v\tExpected: %v\n", loc, expected) 32 | } 33 | if math.Abs(loc.Lat-expected.Lat) > eps { 34 | t.Fatalf("Got: %v\tExpected: %v\n", loc, expected) 35 | } 36 | } 37 | 38 | func TestReverseGeocode(t *testing.T) { 39 | ts := testServer(reverseResp) 40 | defer ts.Close() 41 | 42 | code := "US" 43 | state := "DC" 44 | lat := 38.88669 45 | lng := -77.09464 46 | geocoder := Geocoder(key, ts.URL) 47 | addr, err := geocoder.ReverseGeocode(lat, lng) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | 52 | if addr == nil { 53 | t.Fatalf("Location: lat:%f, lng:%f - Not Found\n", lat, lng) 54 | } 55 | 56 | if addr.CountryCode != code { 57 | t.Fatalf("Got: %v\tExpected: %v\n", addr.CountryCode, code) 58 | } 59 | if addr.State != state { 60 | t.Fatalf("Got: %v\tExpected: %v\n", addr.State, state) 61 | } 62 | } 63 | 64 | func testServer(response string) *httptest.Server { 65 | return httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 66 | resp.Write([]byte(response)) 67 | })) 68 | } 69 | 70 | const ( 71 | eps = 1.0e-5 72 | 73 | geocodeResp = ` 74 | { 75 | "summary": { 76 | "query": "1109 N Highland St, Arlington, VA 22201", 77 | "queryType": "NON_NEAR", 78 | "queryTime": 158, 79 | "numResults": 1, 80 | "offset": 0, 81 | "totalResults": 1, 82 | "fuzzyLevel": 1 83 | }, 84 | "results": [ 85 | { 86 | "type": "Point Address", 87 | "id": "US/PAD/p0/26924656", 88 | "score": 11.51, 89 | "address": { 90 | "streetNumber": "1109", 91 | "streetName": "N Highland St", 92 | "municipalitySubdivision": "Arlington, Clarendon Courthouse", 93 | "municipality": "Arlington", 94 | "countrySecondarySubdivision": "Arlington", 95 | "countryTertiarySubdivision": "Arlington", 96 | "countrySubdivision": "VA", 97 | "postalCode": "22201", 98 | "extendedPostalCode": "222012890", 99 | "countryCode": "US", 100 | "country": "United States Of America", 101 | "countryCodeISO3": "USA", 102 | "freeformAddress": "1109 N Highland St, Arlington, VA 222012890", 103 | "countrySubdivisionName": "Virginia" 104 | }, 105 | "position": { 106 | "lat": 38.88669, 107 | "lon": -77.09464 108 | }, 109 | "viewport": { 110 | "topLeftPoint": { 111 | "lat": 38.88759, 112 | "lon": -77.0958 113 | }, 114 | "btmRightPoint": { 115 | "lat": 38.88579, 116 | "lon": -77.09348 117 | } 118 | }, 119 | "entryPoints": [ 120 | { 121 | "type": "main", 122 | "position": { 123 | "lat": 38.88667, 124 | "lon": -77.09488 125 | } 126 | } 127 | ] 128 | } 129 | ] 130 | }` 131 | 132 | reverseResp = ` 133 | { 134 | "summary": { 135 | "queryTime": 504, 136 | "numResults": 1 137 | }, 138 | "addresses": [ 139 | { 140 | "address": { 141 | "buildingNumber": "414", 142 | "streetNumber": "414", 143 | "routeNumbers": [], 144 | "street": "Seward Sq SE", 145 | "streetName": "Seward Sq SE", 146 | "streetNameAndNumber": "414 Seward Sq SE", 147 | "countryCode": "US", 148 | "countrySubdivision": "DC", 149 | "countrySecondarySubdivision": "District of Columbia", 150 | "countryTertiarySubdivision": "Washington", 151 | "municipality": "Washington", 152 | "postalCode": "20003", 153 | "country": "United States Of America", 154 | "countryCodeISO3": "USA", 155 | "freeformAddress": "414 Seward Sq SE, Washington, DC 20003", 156 | "countrySubdivisionName": "District of Columbia" 157 | }, 158 | "position": "38.886540,-77.000001" 159 | } 160 | ] 161 | }` 162 | ) 163 | -------------------------------------------------------------------------------- /geocod/geocoder_test.go: -------------------------------------------------------------------------------- 1 | package geocod 2 | 3 | import ( 4 | "math" 5 | "net/http" 6 | "net/http/httptest" 7 | "os" 8 | "testing" 9 | 10 | geo "github.com/codingsince1985/geo-golang" 11 | ) 12 | 13 | var key = os.Getenv("GECOD_API_KEY") 14 | 15 | func TestGeocode(t *testing.T) { 16 | ts := testServer(geocodeResp) 17 | defer ts.Close() 18 | 19 | address := "1109 N Highland St, Arlington VA" 20 | geocoder := Geocoder(key, ts.URL) 21 | loc, err := geocoder.Geocode(address) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | if loc == nil { 27 | t.Fatalf("Address: %s - Not Found\n", address) 28 | } 29 | expected := geo.Location{Lng: -77.094733, Lat: 38.886665} 30 | if math.Abs(loc.Lng-expected.Lng) > eps { 31 | t.Fatalf("Got: %v\tExpected: %v\n", loc, expected) 32 | } 33 | if math.Abs(loc.Lat-expected.Lat) > eps { 34 | t.Fatalf("Got: %v\tExpected: %v\n", loc, expected) 35 | } 36 | } 37 | 38 | func TestReverseGeocode(t *testing.T) { 39 | ts := testServer(reverseResp) 40 | defer ts.Close() 41 | 42 | code := "US" 43 | state := "DC" 44 | lat := 38.886665 45 | lng := -77.094733 46 | geocoder := Geocoder(key, ts.URL) 47 | addr, err := geocoder.ReverseGeocode(lat, lng) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | 52 | if addr == nil { 53 | t.Fatalf("Location: lat:%f, lng:%f - Not Found\n", lat, lng) 54 | } 55 | 56 | if addr.CountryCode != code { 57 | t.Fatalf("Got: %v\tExpected: %v\n", addr.CountryCode, code) 58 | } 59 | if addr.State != state { 60 | t.Fatalf("Got: %v\tExpected: %v\n", addr.State, state) 61 | } 62 | } 63 | 64 | func testServer(response string) *httptest.Server { 65 | return httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 66 | resp.Write([]byte(response)) 67 | })) 68 | } 69 | 70 | const ( 71 | eps = 1.0e-5 72 | 73 | geocodeResp = ` 74 | { 75 | "input": { 76 | "address_components": { 77 | "number": "1109", 78 | "predirectional": "N", 79 | "street": "Highland", 80 | "suffix": "St", 81 | "formatted_street": "N Highland St", 82 | "city": "Arlington", 83 | "state": "VA", 84 | "zip": "22201", 85 | "country": "US" 86 | }, 87 | "formatted_address": "1109 N Highland St, Arlington, VA 22201" 88 | }, 89 | "results": [ 90 | { 91 | "address_components": { 92 | "number": "1109", 93 | "predirectional": "N", 94 | "street": "Highland", 95 | "suffix": "St", 96 | "formatted_street": "N Highland St", 97 | "city": "Arlington", 98 | "county": "Arlington County", 99 | "state": "VA", 100 | "zip": "22201", 101 | "country": "US" 102 | }, 103 | "formatted_address": "1109 N Highland St, Arlington, VA 22201", 104 | "location": { 105 | "lat": 38.886665, 106 | "lng": -77.094733 107 | }, 108 | "accuracy": 1, 109 | "accuracy_type": "rooftop", 110 | "source": "Virginia GIS Clearinghouse" 111 | } 112 | ] 113 | }` 114 | 115 | reverseResp = ` 116 | { 117 | "results": [ 118 | { 119 | "address_components": { 120 | "number": "500", 121 | "street": "H", 122 | "suffix": "St", 123 | "postdirectional": "NE", 124 | "formatted_street": "H St NE", 125 | "city": "Washington", 126 | "county": "District of Columbia", 127 | "state": "DC", 128 | "zip": "20002", 129 | "country": "US" 130 | }, 131 | "formatted_address": "500 H St NE, Washington, DC 20002", 132 | "location": { 133 | "lat": 38.900203, 134 | "lng": -76.999507 135 | }, 136 | "accuracy": 1, 137 | "accuracy_type": "nearest_street", 138 | "source": "TIGER/Line® dataset from the US Census Bureau" 139 | }, 140 | { 141 | "address_components": { 142 | "number": "800", 143 | "street": "5th", 144 | "suffix": "St", 145 | "postdirectional": "NE", 146 | "formatted_street": "5th St NE", 147 | "city": "Washington", 148 | "county": "District of Columbia", 149 | "state": "DC", 150 | "zip": "20002", 151 | "country": "US" 152 | }, 153 | "formatted_address": "800 5th St NE, Washington, DC 20002", 154 | "location": { 155 | "lat": 38.900203, 156 | "lng": -76.999507 157 | }, 158 | "accuracy": 0.18, 159 | "accuracy_type": "nearest_street", 160 | "source": "TIGER/Line® dataset from the US Census Bureau" 161 | }, 162 | { 163 | "address_components": { 164 | "number": "474", 165 | "street": "H", 166 | "suffix": "St", 167 | "postdirectional": "NE", 168 | "formatted_street": "H St NE", 169 | "city": "Washington", 170 | "county": "District of Columbia", 171 | "state": "DC", 172 | "zip": "20002", 173 | "country": "US" 174 | }, 175 | "formatted_address": "474 H St NE, Washington, DC 20002", 176 | "location": { 177 | "lat": 38.900205, 178 | "lng": -76.99994 179 | }, 180 | "accuracy": 0.18, 181 | "accuracy_type": "nearest_street", 182 | "source": "TIGER/Line® dataset from the US Census Bureau" 183 | } 184 | ] 185 | }` 186 | ) 187 | -------------------------------------------------------------------------------- /frenchapigouv/geocoder_test.go: -------------------------------------------------------------------------------- 1 | package frenchapigouv_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "net/http" 8 | "net/http/httptest" 9 | 10 | "github.com/codingsince1985/geo-golang" 11 | "github.com/codingsince1985/geo-golang/frenchapigouv" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestGeocode(t *testing.T) { 16 | ts := testServer(response1) 17 | defer ts.Close() 18 | 19 | geocoder := frenchapigouv.GeocoderWithURL(ts.URL + "/") 20 | location, err := geocoder.Geocode("Champ de Mars, 5 Avenue Anatole France, 75007 Paris") 21 | assert.Nil(t, err) 22 | assert.Equal(t, geo.Location{Lat: 48.859831, Lng: 2.328123}, *location) 23 | } 24 | 25 | func TestGeocodeWithNoResult(t *testing.T) { 26 | ts := testServer(response2) 27 | defer ts.Close() 28 | 29 | geocoder := frenchapigouv.GeocoderWithURL(ts.URL + "/") 30 | location, err := geocoder.Geocode("nowhere") 31 | assert.Nil(t, err) 32 | assert.Nil(t, location) 33 | } 34 | 35 | func TestReverseGeocode(t *testing.T) { 36 | ts := testServer(response1) 37 | defer ts.Close() 38 | 39 | geocoder := frenchapigouv.GeocoderWithURL(ts.URL + "/") 40 | address, err := geocoder.ReverseGeocode(48.859831, 2.328123) 41 | assert.Nil(t, err) 42 | assert.True(t, strings.HasPrefix(address.FormattedAddress, "5, Quai Anatole France,")) 43 | assert.Equal(t, "Île-de-France", address.State) 44 | assert.Equal(t, "Paris", address.County) 45 | } 46 | 47 | func TestReverseGeocodeWithTypeStreet(t *testing.T) { 48 | ts := testServer(responseStreet) 49 | defer ts.Close() 50 | geocoder := frenchapigouv.GeocoderWithURL(ts.URL + "/") 51 | address, err := geocoder.ReverseGeocode(50.720114, 3.156717) 52 | assert.Nil(t, err) 53 | assert.True(t, strings.HasPrefix(address.FormattedAddress, "Rue des Anges,")) 54 | assert.Equal(t, "Hauts-de-France", address.State) 55 | assert.Equal(t, "Nord", address.County) 56 | } 57 | 58 | func TestReverseGeocodeWithTypeLocality(t *testing.T) { 59 | ts := testServer(responseLocality) 60 | defer ts.Close() 61 | geocoder := frenchapigouv.GeocoderWithURL(ts.URL + "/") 62 | address, err := geocoder.ReverseGeocode(44.995637, 1.646584) 63 | assert.Nil(t, err) 64 | assert.True(t, strings.HasPrefix(address.FormattedAddress, "Route de Saint Denis les Martel (Les Quatre-Routes-du-Lot),")) 65 | assert.Equal(t, "Occitanie", address.State) 66 | assert.Equal(t, "Lot", address.County) 67 | } 68 | 69 | func TestReverseGeocodeWithNoResult(t *testing.T) { 70 | ts := testServer(response2) 71 | defer ts.Close() 72 | 73 | geocoder := frenchapigouv.GeocoderWithURL(ts.URL + "/") 74 | addr, err := geocoder.ReverseGeocode(0, 0.34) 75 | assert.Nil(t, addr) 76 | assert.Nil(t, err) 77 | } 78 | 79 | func TestReverseGeocodeWithNoResultByDefaultPoints(t *testing.T) { 80 | ts := testServer(response3) 81 | defer ts.Close() 82 | 83 | geocoder := frenchapigouv.GeocoderWithURL(ts.URL + "/") 84 | addr, err := geocoder.ReverseGeocode(0, 0) 85 | assert.Nil(t, addr) 86 | assert.Nil(t, err) 87 | } 88 | 89 | func testServer(response string) *httptest.Server { 90 | return httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 91 | resp.Write([]byte(response)) 92 | })) 93 | } 94 | 95 | const ( 96 | response1 = `[ 97 | { 98 | "type": "FeatureCollection", 99 | "version": "draft", 100 | "features": [{ 101 | "type": "Feature", 102 | "geometry": { 103 | "type": "Point", 104 | "coordinates": [2.328123, 48.859831] 105 | }, 106 | "properties": { 107 | "label": "5 Quai Anatole France 75007 Paris", 108 | "score": 0.4882581475128644, 109 | "housenumber": "5", 110 | "citycode": "75107", 111 | "context": "75, Paris, Île-de-France", 112 | "postcode": "75007", 113 | "name": "5 Quai Anatole France", 114 | "id": "ADRNIVX_0000000270768224", 115 | "y": 6862409.3, 116 | "importance": 0.2765, 117 | "type": "housenumber", 118 | "city": "Paris", 119 | "x": 650705.5, 120 | "street": "Quai Anatole France" 121 | } 122 | }], 123 | "attribution": "BAN", 124 | "licence": "ODbL 1.0", 125 | "query": "Champ de Mars, 5 Avenue Anatole France, 75007 Paris", 126 | "limit": 10 127 | } 128 | ]` 129 | response2 = `{ 130 | "type": "FeatureCollection", 131 | "version": "draft", 132 | "features": [], 133 | "attribution": "BAN", 134 | "licence": "ODbL 1.0", 135 | "limit": 1 136 | }` 137 | response3 = `{ 138 | "type": "FeatureCollection", 139 | "version": "draft", 140 | "features": [{ 141 | "type": "Feature", 142 | "geometry": { 143 | "type": "Point", 144 | "coordinates": [0.0, 0.0] 145 | }, 146 | "properties": { 147 | "label": "baninfo", 148 | "score": 1.0, 149 | "name": "baninfo", 150 | "id": "db", 151 | "type": "info", 152 | "context": "20181125T223127", 153 | "distance": 0 154 | } 155 | }], 156 | "attribution": "BAN", 157 | "licence": "ODbL 1.0", 158 | "limit": 1 159 | }` 160 | responseStreet = `{ 161 | "type": "FeatureCollection", 162 | "version": "draft", 163 | "features": [{ 164 | "type": "Feature", 165 | "geometry": { 166 | "type": "Point", 167 | "coordinates": [3.156717,50.720114] 168 | }, 169 | "properties": { 170 | "label": "Rue des Anges 59200 Tourcoing", 171 | "score": 0.7319487603305784, 172 | "id": "59599_0310", 173 | "name": "Rue des Anges", 174 | "postcode": "59200", 175 | "citycode": "59599", 176 | "x": 711087.23, 177 | "y": 7069277.25, 178 | "city": "Tourcoing", 179 | "context": "59, Nord, Hauts-de-France", 180 | "type": "street", 181 | "importance": 0.6878 182 | } 183 | }], 184 | "attribution": "BAN", 185 | "licence": "ETALAB-2.0", 186 | "query": "11B Rue des Anges Tourcoing 59200", 187 | "limit": 1 188 | }` 189 | responseLocality = `{ 190 | "type": "FeatureCollection", 191 | "version": "draft", 192 | "features": [{ 193 | "type": "Feature", 194 | "geometry": { 195 | "type": "Point", 196 | "coordinates": [1.646584,44.995637] 197 | }, 198 | "properties": { 199 | "label": "Route de Saint Denis les Martel (Les Quatre-Routes-du-Lot) 46110 Le Vignon-en-Quercy", 200 | "score": 0.8454463636363636, 201 | "type": "locality", 202 | "importance": 0.29991, 203 | "id": "46232_0023", 204 | "name": "Route de Saint Denis les Martel (Les Quatre-Routes-du-Lot)", 205 | "postcode": "46110", 206 | "citycode": "46232", 207 | "oldcitycode": "46232", 208 | "x": 593348.58, 209 | "y": 6433848.43, 210 | "city": "Le Vignon-en-Quercy", 211 | "oldcity": "Les Quatre-Routes-du-Lot", 212 | "context": "46, Lot, Occitanie" 213 | } 214 | }], 215 | "attribution": "BAN", 216 | "licence": "ETALAB-2.0", 217 | "query": "Route de Saint-Denis-lès-Martel", 218 | "limit": 1 219 | }` 220 | ) 221 | -------------------------------------------------------------------------------- /baidu/baidu.go: -------------------------------------------------------------------------------- 1 | package baidu 2 | 3 | import ( 4 | "fmt" 5 | "github.com/codingsince1985/geo-golang" 6 | "strings" 7 | ) 8 | 9 | var ( 10 | languageList = []string{"el", "gu", "en", "vi", "ca", "it", "iw", "sv", "eu", "ar", "cs", "gl", "id", "es", "en-GB", "ru", "sr", "nl", "pt", "tr", "tl", "lv", "en-AU", "lt", "th", "ro", "fil", "ta", "fr", "bg", "hr", "bn", "de", "hu", "fa", "hi", "pt-BR", "fi", "da", "ja", "te", "pt-PT", "ml", "ko", "kn", "sk", "zh-CN", "pl", "uk", "sl", "mr", "local"} 11 | ) 12 | 13 | type ( 14 | baseURL string 15 | 16 | //Response payload 17 | //{'result': {'addressComponent': {'adcode': '310101', 18 | // 'city': '上海市', 19 | // 'city_level': 2, 20 | // 'country': '中国', 21 | // 'country_code': 0, 22 | // 'country_code_iso': 'CHN', 23 | // 'country_code_iso2': 'CN', 24 | // 'direction': '东北', 25 | // 'distance': '91', 26 | // 'district': '黄浦区', 27 | // 'province': '上海市', 28 | // 'street': '中山南路', 29 | // 'street_number': '187', 30 | // 'town': '', 31 | // 'town_code': ''}, 32 | // 'business': '外滩,陆家嘴,董家渡', 33 | // 'cityCode': 289, 34 | // 'formatted_address': '上海市黄浦区中山南路187', 35 | // 'location': {'lat': 31.22932842411674, 'lng': 121.50989077799083}, 36 | // 'poiRegions': [], 37 | // 'pois': [], 38 | // 'roads': [], 39 | // 'sematic_description': ''}, 40 | //'status': 0} 41 | geocodeResponse struct { 42 | Result struct { 43 | AddressComponent struct { 44 | AdCode string `json:"adcode"` 45 | City string `json:"city"` 46 | CityLevel int `json:"city_level"` 47 | Country string `json:"country"` 48 | CountryCode int `json:"country_code"` 49 | CountryCodeIso string `json:"country_code_iso"` 50 | CountryCodeIso2 string `json:"country_code_iso2"` 51 | Direction string `json:"direction"` 52 | Distance string `json:"distance"` 53 | District string `json:"district"` 54 | Province string `json:"province"` 55 | Street string `json:"street"` 56 | StreetNumber string `json:"street_number"` 57 | Town string `json:"town"` 58 | TownCode string `json:"town_code"` 59 | } `json:"addressComponent"` 60 | Business string `json:"business"` 61 | CityCode int `json:"cityCode"` 62 | FormattedAddress string `json:"formatted_address"` 63 | Location struct { 64 | Lat float64 `json:"lat"` 65 | Lng float64 `json:"lng"` 66 | } `json:"location"` 67 | PoiRegions []interface{} `json:"poiRegions"` 68 | Pois []interface{} `json:"pois"` 69 | Roads []interface{} `json:"roads"` 70 | SematicDescription string `json:"sematic_description"` 71 | } `json:"result"` 72 | Status int `json:"status"` 73 | } 74 | ) 75 | 76 | const ( 77 | statusOK = 0 78 | ) 79 | 80 | var lang string 81 | var coordtype string 82 | 83 | // Geocoder constructs Baidu geocoder 84 | // 85 | // language: Baidu Map's API uses Chinese (zh-CN) by default. but it supports language argument to specify which language it 86 | // will use. If some fields don't have the target language version, it will be filled with Chinese version by default. 87 | // Acceptable languages arguments are : 88 | // "el", "gu", "en", "vi", "ca", "it", "iw", "sv", "eu", "ar", "cs", "gl", "id", "es", "en-GB", "ru", "sr", "nl", "pt", 89 | // "tr", "tl", "lv", "en-AU", "lt", "th", "ro", "fil", "ta", "fr", "bg", "hr", "bn", "de", "hu", "fa", "hi", "pt-BR", 90 | // "fi", "da", "ja", "te", "pt-PT", "ml", "ko", "kn", "sk", "zh-CN", "pl", "uk", "sl", "mr", "local" 91 | // 92 | // "local" is a special argument for language specifying, which means the language it uses depends on your location. 93 | // 94 | // coordtype: 95 | // According to league reasons, Chinese geo service providers uses encrypted coordinate system (GCJ02ll, BD09ll, BD09mc, 96 | // etc.) other than the World Geodetic System (WGS, especially WGS84ll). 97 | // Acceptable coordtype arguments are : 98 | // "bd09ll", "bd09mc", "gcj02ll", "wgs84ll" 99 | // default: "bd09ll" 100 | // You can use https://api.map.baidu.com/geoconv/v1/?coords=LONGITUDE,LATITUDE&from=1&to=5&ak=AK to convert from WGS84ll 101 | // to BD09ll coordination. API document: https://lbsyun.baidu.com/index.php?title=webapi/guide/changeposition 102 | func Geocoder(apiKey string, language string, coordtype string, baseURLs ...string) geo.Geocoder { 103 | if lang != "" { 104 | for _, item := range languageList { 105 | if language == item { 106 | lang = fmt.Sprintf("&language=%s", language) 107 | } 108 | } 109 | } 110 | switch coordtype { 111 | case "bd09ll": 112 | coordtype = "bd09ll" 113 | case "bd09mc": 114 | coordtype = "bd09mc" 115 | case "gcj02ll": 116 | coordtype = "gcj02ll" 117 | case "wgs84ll": 118 | coordtype = "wgs84ll" 119 | default: 120 | coordtype = "bd09ll" 121 | } 122 | 123 | return geo.HTTPGeocoder{ 124 | EndpointBuilder: baseURL(getURL(apiKey, baseURLs...)), 125 | ResponseParserFactory: func() geo.ResponseParser { return &geocodeResponse{} }, 126 | } 127 | } 128 | 129 | func getURL(apiKey string, baseURLs ...string) string { 130 | if len(baseURLs) > 0 { 131 | return baseURLs[0] 132 | } 133 | return fmt.Sprintf("https://api.map.baidu.com/*/v3/?ak=%s&", apiKey) 134 | } 135 | 136 | // GeocodeURL https://api.map.baidu.com/geocoding/v3/?ak=APPKEY&output=json&address=ADDRESS 137 | func (b baseURL) GeocodeURL(address string) string { 138 | return strings.Replace(string(b), "*", "geocoding", 1) + fmt.Sprintf("output=json&address=%s", address) 139 | } 140 | 141 | // ReverseGeocodeURL https://api.map.baidu.com/reverse_geocoding/v3/?ak=APPKEY&output=json&&coordtype=wgs84ll&location=31.225696563611,121.49884033194 142 | func (b baseURL) ReverseGeocodeURL(l geo.Location) string { 143 | return strings.Replace(string(b), "*", "reverse_geocoding", 1) + fmt.Sprintf("output=json&coordtype=%s&location=%f,%f", coordtype, l.Lat, l.Lng) + lang 144 | } 145 | 146 | func (r *geocodeResponse) Location() (*geo.Location, error) { 147 | var location = &geo.Location{} 148 | if r.Status == 1 { 149 | return nil, nil 150 | } else if r.Status != statusOK { 151 | return nil, fmt.Errorf("geocoding error: %v", r.Status) 152 | } 153 | location.Lat = r.Result.Location.Lat 154 | location.Lng = r.Result.Location.Lng 155 | return location, nil 156 | 157 | } 158 | 159 | func (r *geocodeResponse) Address() (*geo.Address, error) { 160 | if r.Status == 1 { 161 | return nil, nil 162 | } else if r.Status != statusOK { 163 | return nil, fmt.Errorf("reverse geocoding error: %v", r.Status) 164 | } 165 | 166 | addr := parseBaiduResult(r) 167 | 168 | return addr, nil 169 | } 170 | 171 | func parseBaiduResult(r *geocodeResponse) *geo.Address { 172 | addr := &geo.Address{} 173 | res := r.Result 174 | addr.FormattedAddress = res.FormattedAddress 175 | addr.HouseNumber = res.AddressComponent.StreetNumber 176 | addr.Street = res.AddressComponent.Street 177 | addr.Suburb = res.AddressComponent.District 178 | addr.City = res.AddressComponent.City 179 | addr.State = res.AddressComponent.Province 180 | addr.Country = res.AddressComponent.Country 181 | addr.CountryCode = res.AddressComponent.CountryCodeIso 182 | 183 | if (*addr == geo.Address{}) { 184 | return nil 185 | } 186 | return addr 187 | } 188 | -------------------------------------------------------------------------------- /mapzen/geocoder_test.go: -------------------------------------------------------------------------------- 1 | package mapzen 2 | 3 | import ( 4 | "math" 5 | "net/http" 6 | "net/http/httptest" 7 | "os" 8 | "testing" 9 | 10 | geo "github.com/codingsince1985/geo-golang" 11 | ) 12 | 13 | var key = os.Getenv("MAPZEN_API_KEY") 14 | 15 | func TestGeocode(t *testing.T) { 16 | ts := testServer(geocodeResp) 17 | defer ts.Close() 18 | 19 | address := "1109 N Highland St, Arlington VA" 20 | geocoder := Geocoder(key, ts.URL) 21 | loc, err := geocoder.Geocode(address) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | if loc == nil { 27 | t.Fatalf("Address: %s - Not Found\n", address) 28 | } 29 | expected := geo.Location{Lng: -77.094733, Lat: 38.886665} 30 | if math.Abs(loc.Lng-expected.Lng) > eps { 31 | t.Fatalf("Got: %v\tExpected: %v\n", loc, expected) 32 | } 33 | if math.Abs(loc.Lat-expected.Lat) > eps { 34 | t.Fatalf("Got: %v\tExpected: %v\n", loc, expected) 35 | } 36 | } 37 | 38 | func TestReverseGeocode(t *testing.T) { 39 | ts := testServer(reverseResp) 40 | defer ts.Close() 41 | 42 | house := "1109" 43 | code := "USA" 44 | state := "Virginia" 45 | stateCode := "VA" 46 | lat := 38.886665 47 | lng := -77.094733 48 | geocoder := Geocoder(key, ts.URL) 49 | addr, err := geocoder.ReverseGeocode(lat, lng) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | if addr == nil { 55 | t.Fatalf("Location: lat:%f, lng:%f - Not Found\n", lat, lng) 56 | } 57 | 58 | if addr.HouseNumber != house { 59 | t.Fatalf("Got: %v\tExpected: %v\n", addr.HouseNumber, house) 60 | } 61 | if addr.CountryCode != code { 62 | t.Fatalf("Got: %v\tExpected: %v\n", addr.CountryCode, code) 63 | } 64 | if addr.State != state { 65 | t.Fatalf("Got: %v\tExpected: %v\n", addr.State, state) 66 | } 67 | if addr.StateCode != stateCode { 68 | t.Fatalf("Got: %v\tExpected: %v\n", addr.StateCode, stateCode) 69 | } 70 | } 71 | 72 | func testServer(response string) *httptest.Server { 73 | return httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 74 | resp.Write([]byte(response)) 75 | })) 76 | } 77 | 78 | const ( 79 | eps = 1.0e-5 80 | 81 | geocodeResp = ` 82 | { 83 | "geocoding": { 84 | "version": "0.2", 85 | "attribution": "https://search.mapzen.com/v1/attribution", 86 | "query": { 87 | "text": "1109 N Highland St, Arlington VA", 88 | "parsed_text": { 89 | "number": "1109", 90 | "street": "n highland st", 91 | "city": "arlington", 92 | "state": "va" 93 | }, 94 | "size": 1, 95 | "private": false, 96 | "lang": { 97 | "name": "English", 98 | "iso6391": "en", 99 | "iso6393": "eng", 100 | "defaulted": true 101 | }, 102 | "querySize": 20 103 | }, 104 | "engine": { 105 | "name": "Pelias", 106 | "author": "Mapzen", 107 | "version": "1.0" 108 | }, 109 | "timestamp": 1499034434562 110 | }, 111 | "type": "FeatureCollection", 112 | "features": [ 113 | { 114 | "type": "Feature", 115 | "geometry": { 116 | "type": "Point", 117 | "coordinates": [ 118 | -77.094733, 119 | 38.886665 120 | ] 121 | }, 122 | "properties": { 123 | "id": "us/va/statewide:7120c09d09e25349", 124 | "gid": "openaddresses:address:us/va/statewide:7120c09d09e25349", 125 | "layer": "address", 126 | "source": "openaddresses", 127 | "source_id": "us/va/statewide:7120c09d09e25349", 128 | "name": "1109 N Highland St", 129 | "housenumber": "1109", 130 | "street": "N Highland St", 131 | "postalcode": "22201", 132 | "confidence": 1, 133 | "match_type": "exact", 134 | "accuracy": "point", 135 | "country": "United States", 136 | "country_gid": "whosonfirst:country:85633793", 137 | "country_a": "USA", 138 | "region": "Virginia", 139 | "region_gid": "whosonfirst:region:85688747", 140 | "region_a": "VA", 141 | "county": "Arlington County", 142 | "county_gid": "whosonfirst:county:102085953", 143 | "locality": "Arlington", 144 | "locality_gid": "whosonfirst:locality:101729469", 145 | "neighbourhood": "Courthouse", 146 | "neighbourhood_gid": "whosonfirst:neighbourhood:85811147", 147 | "label": "1109 N Highland St, Arlington, VA, USA" 148 | } 149 | } 150 | ], 151 | "bbox": [ 152 | -77.094733, 153 | 38.886665, 154 | -77.094733, 155 | 38.886665 156 | ] 157 | } 158 | ` 159 | 160 | reverseResp = ` 161 | { 162 | "geocoding": { 163 | "version": "0.2", 164 | "attribution": "https://search.mapzen.com/v1/attribution", 165 | "query": { 166 | "size": 1, 167 | "private": false, 168 | "point.lat": 38.886665, 169 | "point.lon": -77.094733, 170 | "boundary.circle.radius": 1, 171 | "boundary.circle.lat": 38.886665, 172 | "boundary.circle.lon": -77.094733, 173 | "lang": { 174 | "name": "English", 175 | "iso6391": "en", 176 | "iso6393": "eng", 177 | "defaulted": true 178 | }, 179 | "querySize": 20 180 | }, 181 | "engine": { 182 | "name": "Pelias", 183 | "author": "Mapzen", 184 | "version": "1.0" 185 | }, 186 | "timestamp": 1499034728798 187 | }, 188 | "type": "FeatureCollection", 189 | "features": [ 190 | { 191 | "type": "Feature", 192 | "geometry": { 193 | "type": "Point", 194 | "coordinates": [ 195 | -77.094733, 196 | 38.886665 197 | ] 198 | }, 199 | "properties": { 200 | "id": "us/va/statewide:7120c09d09e25349", 201 | "gid": "openaddresses:address:us/va/statewide:7120c09d09e25349", 202 | "layer": "address", 203 | "source": "openaddresses", 204 | "source_id": "us/va/statewide:7120c09d09e25349", 205 | "name": "1109 N Highland St", 206 | "housenumber": "1109", 207 | "street": "N Highland St", 208 | "postalcode": "22201", 209 | "confidence": 1, 210 | "distance": 0, 211 | "accuracy": "point", 212 | "country": "United States", 213 | "country_gid": "whosonfirst:country:85633793", 214 | "country_a": "USA", 215 | "region": "Virginia", 216 | "region_gid": "whosonfirst:region:85688747", 217 | "region_a": "VA", 218 | "county": "Arlington County", 219 | "county_gid": "whosonfirst:county:102085953", 220 | "locality": "Arlington", 221 | "locality_gid": "whosonfirst:locality:101729469", 222 | "neighbourhood": "Courthouse", 223 | "neighbourhood_gid": "whosonfirst:neighbourhood:85811147", 224 | "label": "1109 N Highland St, Arlington, VA, USA" 225 | } 226 | } 227 | ], 228 | "bbox": [ 229 | -77.094733, 230 | 38.886665, 231 | -77.094733, 232 | 38.886665 233 | ] 234 | }` 235 | ) 236 | -------------------------------------------------------------------------------- /mapquest/open/geocoder_test.go: -------------------------------------------------------------------------------- 1 | package open_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/codingsince1985/geo-golang" 11 | "github.com/codingsince1985/geo-golang/mapquest/open" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | var key = os.Getenv("MAPQUEST_OPEN_KEY") 16 | 17 | func TestGeocode(t *testing.T) { 18 | ts := testServer(response1) 19 | defer ts.Close() 20 | 21 | geocoder := open.Geocoder(key, ts.URL+"/") 22 | location, err := geocoder.Geocode("60 Collins St, Melbourne VIC 3000") 23 | assert.NoError(t, err) 24 | assert.Equal(t, geo.Location{Lat: -37.813743, Lng: 144.971745}, *location) 25 | } 26 | 27 | func TestReverseGeocode(t *testing.T) { 28 | ts := testServer(response2) 29 | defer ts.Close() 30 | 31 | geocoder := open.Geocoder(key, ts.URL+"/") 32 | address, err := geocoder.ReverseGeocode(-37.813743, 144.971745) 33 | assert.NoError(t, err) 34 | assert.True(t, strings.HasPrefix(address.FormattedAddress, "Exhibition Street")) 35 | } 36 | 37 | func TestReverseGeocodeWithNoResult(t *testing.T) { 38 | ts := testServer(response3) 39 | defer ts.Close() 40 | 41 | geocoder := open.Geocoder(key, ts.URL+"/") 42 | //geocoder := open.Geocoder(key) 43 | addr, err := geocoder.ReverseGeocode(-37.813743, 164.971745) 44 | assert.Nil(t, err) 45 | assert.Nil(t, addr) 46 | } 47 | 48 | func testServer(response string) *httptest.Server { 49 | return httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 50 | resp.Write([]byte(response)) 51 | })) 52 | } 53 | 54 | const ( 55 | response1 = `{ 56 | "info":{ 57 | "statuscode":0, 58 | "copyright":{ 59 | "text":"\u00A9 2016 MapQuest, Inc.", 60 | "imageUrl":"http://api.mqcdn.com/res/mqlogo.gif", 61 | "imageAltText":"\u00A9 2016 MapQuest, Inc." 62 | }, 63 | "messages":[ 64 | 65 | ] 66 | }, 67 | "options":{ 68 | "maxResults":-1, 69 | "thumbMaps":true, 70 | "ignoreLatLngInput":false 71 | }, 72 | "results":[ 73 | { 74 | "providedLocation":{ 75 | "location":"60 Collins St, Melbourne VIC 3000" 76 | }, 77 | "locations":[ 78 | { 79 | "street":"60 Collins Street", 80 | "adminArea6":"Melbourne", 81 | "adminArea6Type":"Neighborhood", 82 | "adminArea5":"Melbourne", 83 | "adminArea5Type":"City", 84 | "adminArea4":"City of Melbourne", 85 | "adminArea4Type":"County", 86 | "adminArea3":"Victoria", 87 | "adminArea3Type":"State", 88 | "adminArea1":"AU", 89 | "adminArea1Type":"Country", 90 | "postalCode":"3000", 91 | "geocodeQualityCode":"P1XAA", 92 | "geocodeQuality":"POINT", 93 | "dragPoint":false, 94 | "sideOfStreet":"N", 95 | "linkId":"0", 96 | "unknownInput":"", 97 | "type":"s", 98 | "latLng":{ 99 | "lat":-37.813743, 100 | "lng":144.971745 101 | }, 102 | "displayLatLng":{ 103 | "lat":-37.813743, 104 | "lng":144.971745 105 | }, 106 | "mapUrl":"http://open.mapquestapi.com/staticmap/v4/getmap?key=Fmjtd|luu8216t21,rn=o5-94255w&type=map&size=225,160&pois=purple-1,-37.8137433689794,144.971745104488,0,0,|¢er=-37.8137433689794,144.971745104488&zoom=15&rand=-2113669715" 107 | } 108 | ] 109 | } 110 | ] 111 | }` 112 | response2 = `{ 113 | "info":{ 114 | "statuscode":0, 115 | "copyright":{ 116 | "text":"\u00A9 2016 MapQuest, Inc.", 117 | "imageUrl":"http://api.mqcdn.com/res/mqlogo.gif", 118 | "imageAltText":"\u00A9 2016 MapQuest, Inc." 119 | }, 120 | "messages":[ 121 | 122 | ] 123 | }, 124 | "options":{ 125 | "maxResults":1, 126 | "thumbMaps":true, 127 | "ignoreLatLngInput":false 128 | }, 129 | "results":[ 130 | { 131 | "providedLocation":{ 132 | "latLng":{ 133 | "lat":-37.813743, 134 | "lng":144.971745 135 | } 136 | }, 137 | "locations":[ 138 | { 139 | "street":"Exhibition Street", 140 | "adminArea6":"Melbourne", 141 | "adminArea6Type":"Neighborhood", 142 | "adminArea5":"Melbourne", 143 | "adminArea5Type":"City", 144 | "adminArea4":"City of Melbourne", 145 | "adminArea4Type":"County", 146 | "adminArea3":"Victoria", 147 | "adminArea3Type":"State", 148 | "adminArea1":"AU", 149 | "adminArea1Type":"Country", 150 | "postalCode":"3000", 151 | "geocodeQualityCode":"L1AAA", 152 | "geocodeQuality":"ADDRESS", 153 | "dragPoint":false, 154 | "sideOfStreet":"N", 155 | "linkId":"0", 156 | "unknownInput":"", 157 | "type":"s", 158 | "latLng":{ 159 | "lat":-37.813621, 160 | "lng":144.97161 161 | }, 162 | "displayLatLng":{ 163 | "lat":-37.813621, 164 | "lng":144.97161 165 | }, 166 | "mapUrl":"http://open.mapquestapi.com/staticmap/v4/getmap?key=Fmjtd|luu8216t21,rn=o5-94255w&type=map&size=225,160&pois=purple-1,-37.81362105,144.971609569165,0,0,|¢er=-37.81362105,144.971609569165&zoom=15&rand=2088657841" 167 | } 168 | ] 169 | } 170 | ] 171 | }` 172 | response3 = `{ 173 | "info":{ 174 | "statuscode":0, 175 | "copyright":{ 176 | "text":"\u00A9 2016 MapQuest, Inc.", 177 | "imageUrl":"http://api.mqcdn.com/res/mqlogo.gif", 178 | "imageAltText":"\u00A9 2016 MapQuest, Inc." 179 | }, 180 | "messages":[ 181 | 182 | ] 183 | }, 184 | "options":{ 185 | "maxResults":1, 186 | "thumbMaps":true, 187 | "ignoreLatLngInput":false 188 | }, 189 | "results":[ 190 | { 191 | "providedLocation":{ 192 | "latLng":{ 193 | "lat":-37.813743, 194 | "lng":164.971745 195 | } 196 | }, 197 | "locations":[ 198 | { 199 | "street":"", 200 | "adminArea6":"", 201 | "adminArea6Type":"Neighborhood", 202 | "adminArea5":"", 203 | "adminArea5Type":"City", 204 | "adminArea4":"", 205 | "adminArea4Type":"County", 206 | "adminArea3":"", 207 | "adminArea3Type":"State", 208 | "adminArea1":"", 209 | "adminArea1Type":"Country", 210 | "postalCode":"", 211 | "geocodeQualityCode":"XXXXX", 212 | "geocodeQuality":"UNKNOWN", 213 | "dragPoint":false, 214 | "sideOfStreet":"N", 215 | "linkId":"0", 216 | "unknownInput":"", 217 | "type":"s", 218 | "latLng":{ 219 | "lat":-37.813743, 220 | "lng":164.971745 221 | }, 222 | "displayLatLng":{ 223 | "lat":-37.813743, 224 | "lng":164.971745 225 | }, 226 | "mapUrl":"http://open.mapquestapi.com/staticmap/v4/getmap?key=Fmjtd|luu8216t21,rn=o5-94255w&type=map&size=225,160&pois=purple-1,-37.813743,164.971745,0,0,|¢er=-37.813743,164.971745&zoom=15&rand=342866536" 227 | } 228 | ] 229 | } 230 | ] 231 | }` 232 | ) 233 | -------------------------------------------------------------------------------- /examples/geocoder_example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/codingsince1985/geo-golang" 8 | "github.com/codingsince1985/geo-golang/amap" 9 | "github.com/codingsince1985/geo-golang/arcgis" 10 | "github.com/codingsince1985/geo-golang/baidu" 11 | "github.com/codingsince1985/geo-golang/bing" 12 | "github.com/codingsince1985/geo-golang/chained" 13 | "github.com/codingsince1985/geo-golang/frenchapigouv" 14 | "github.com/codingsince1985/geo-golang/geocod" 15 | "github.com/codingsince1985/geo-golang/google" 16 | "github.com/codingsince1985/geo-golang/here" 17 | "github.com/codingsince1985/geo-golang/locationiq" 18 | "github.com/codingsince1985/geo-golang/mapbox" 19 | "github.com/codingsince1985/geo-golang/mapquest/nominatim" 20 | "github.com/codingsince1985/geo-golang/mapquest/open" 21 | "github.com/codingsince1985/geo-golang/mapzen" 22 | "github.com/codingsince1985/geo-golang/opencage" 23 | "github.com/codingsince1985/geo-golang/openstreetmap" 24 | "github.com/codingsince1985/geo-golang/pickpoint" 25 | "github.com/codingsince1985/geo-golang/tomtom" 26 | "github.com/codingsince1985/geo-golang/yandex" 27 | ) 28 | 29 | const ( 30 | addr = "Melbourne VIC" 31 | lat, lng = -37.813611, 144.963056 32 | radius = 50 33 | zoom = 18 34 | addrFR = "Champs de Mars Paris" 35 | addrAmap = "北京市海淀区清河街道西二旗西路领秀新硅谷" // China only 36 | latFR, lngFR = 48.854395, 2.304770 37 | latAmap, lngAmap = 40.05510, 116.309866 38 | ) 39 | 40 | func main() { 41 | ExampleGeocoder() 42 | } 43 | 44 | // ExampleGeocoder demonstrates the different geocoding services 45 | func ExampleGeocoder() { 46 | fmt.Println("Google Geocoding API") 47 | try(google.Geocoder(os.Getenv("GOOGLE_API_KEY"))) 48 | 49 | fmt.Println("Mapquest Nominatim") 50 | try(nominatim.Geocoder(os.Getenv("MAPQUEST_NOMINATIM_KEY"))) 51 | 52 | fmt.Println("Mapquest Open streetmaps") 53 | try(open.Geocoder(os.Getenv("MAPQUEST_OPEN_KEY"))) 54 | 55 | fmt.Println("OpenCage Data") 56 | try(opencage.Geocoder(os.Getenv("OPENCAGE_API_KEY"))) 57 | 58 | fmt.Println("HERE Geocoder API") 59 | try(here.Geocoder(os.Getenv("HERE_APP_ID"), os.Getenv("HERE_APP_CODE"), radius)) 60 | 61 | fmt.Println("Bing Geocoding API") 62 | try(bing.Geocoder(os.Getenv("BING_API_KEY"))) 63 | 64 | fmt.Println("Baidu Geocoding API") 65 | try(baidu.Geocoder(os.Getenv("BAIDU_API_KEY"), "en", "bd09ll")) 66 | 67 | fmt.Println("Amap Geocoding API") 68 | tryOnlyForAmap(amap.Geocoder(os.Getenv("AMAP_API_KEY"), radius)) 69 | 70 | fmt.Println("Mapbox API") 71 | try(mapbox.Geocoder(os.Getenv("MAPBOX_API_KEY"))) 72 | 73 | fmt.Println("OpenStreetMap") 74 | try(openstreetmap.Geocoder()) 75 | 76 | fmt.Println("PickPoint") 77 | try(pickpoint.Geocoder(os.Getenv("PICKPOINT_API_KEY"))) 78 | 79 | fmt.Println("LocationIQ") 80 | try(locationiq.Geocoder(os.Getenv("LOCATIONIQ_API_KEY"), zoom)) 81 | 82 | fmt.Println("ArcGIS") 83 | try(arcgis.Geocoder(os.Getenv("ARCGIS_TOKEN"))) 84 | 85 | fmt.Println("geocod.io") 86 | try(geocod.Geocoder(os.Getenv("GEOCOD_API_KEY"))) 87 | 88 | fmt.Println("Mapzen") 89 | try(mapzen.Geocoder(os.Getenv("MAPZEN_API_KEY"))) 90 | 91 | fmt.Println("TomTom") 92 | try(tomtom.Geocoder(os.Getenv("TOMTOM_API_KEY"))) 93 | 94 | fmt.Println("Yandex") 95 | try(yandex.Geocoder(os.Getenv("YANDEX_API_KEY"))) 96 | 97 | // To use only with french locations or addresses, 98 | // or values ​​could be estimated and will be false 99 | fmt.Println("FrenchAPIGouv") 100 | tryOnlyFRData(frenchapigouv.Geocoder()) 101 | 102 | // Chained geocoder will fallback to subsequent geocoders 103 | fmt.Println("ChainedAPI[OpenStreetmap -> Google]") 104 | try(chained.Geocoder( 105 | openstreetmap.Geocoder(), 106 | google.Geocoder(os.Getenv("GOOGLE_API_KEY")), 107 | )) 108 | // Output: Google Geocoding API 109 | // Melbourne VIC location is (-37.813611, 144.963056) 110 | // Address of (-37.813611,144.963056) is 197 Elizabeth St, Melbourne VIC 3000, Australia 111 | // Detailed address: &geo.Address{FormattedAddress:"197 Elizabeth St, Melbourne VIC 3000, Australia", 112 | // Street:"Elizabeth Street", HouseNumber:"197", Suburb:"", Postcode:"3000", State:"Victoria", 113 | // StateDistrict:"Melbourne City", County:"", Country:"Australia", CountryCode:"AU", City:"Melbourne"} 114 | // 115 | // Mapquest Nominatim 116 | // Melbourne VIC location is (-37.814218, 144.963161) 117 | // Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia 118 | // Detailed address: &geo.Address{FormattedAddress:"Melbourne's GPO, Postal Lane, Melbourne, City of Melbourne, 119 | // Greater Melbourne, Victoria, 3000, Australia", Street:"Postal Lane", HouseNumber:"", Suburb:"Melbourne", 120 | // Postcode:"3000", State:"Victoria", StateDistrict:"", County:"City of Melbourne", Country:"Australia", CountryCode:"AU", City:"Melbourne"} 121 | // 122 | // Mapquest Open streetmaps 123 | // Melbourne VIC location is (-37.814218, 144.963161) 124 | // Address of (-37.813611,144.963056) is Elizabeth Street, Melbourne, Victoria, AU 125 | // Detailed address: &geo.Address{FormattedAddress:"Elizabeth Street, 3000, Melbourne, Victoria, AU", 126 | // Street:"Elizabeth Street", HouseNumber:"", Suburb:"", Postcode:"3000", State:"Victoria", StateDistrict:"", 127 | // County:"", Country:"", CountryCode:"AU", City:"Melbourne"} 128 | // 129 | // OpenCage Data 130 | // Melbourne VIC location is (-37.814217, 144.963161) 131 | // Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Melbourne VIC 3000, Australia 132 | // Detailed address: &geo.Address{FormattedAddress:"Melbourne's GPO, Postal Lane, Melbourne VIC 3000, Australia", 133 | // Street:"Postal Lane", HouseNumber:"", Suburb:"Melbourne (3000)", Postcode:"3000", State:"Victoria", 134 | // StateDistrict:"", County:"City of Melbourne", Country:"Australia", CountryCode:"AU", City:"Melbourne"} 135 | // 136 | // HERE Geocoder API 137 | // Melbourne VIC location is (-37.817530, 144.967150) 138 | // Address of (-37.813611,144.963056) is 197 Elizabeth St, Melbourne VIC 3000, Australia 139 | // Detailed address: &geo.Address{FormattedAddress:"197 Elizabeth St, Melbourne VIC 3000, Australia", Street:"Elizabeth St", 140 | // HouseNumber:"197", Suburb:"", Postcode:"3000", State:"Victoria", StateDistrict:"", County:"", Country:"Australia", 141 | // CountryCode:"AUS", City:"Melbourne"} 142 | // 143 | // Bing Geocoding API 144 | // Melbourne VIC location is (-37.824299, 144.977997) 145 | // Address of (-37.813611,144.963056) is Elizabeth St, Melbourne, VIC 3000 146 | // Detailed address: &geo.Address{FormattedAddress:"Elizabeth St, Melbourne, VIC 3000", Street:"Elizabeth St", 147 | // HouseNumber:"", Suburb:"", Postcode:"3000", State:"", StateDistrict:"", County:"", Country:"Australia", CountryCode:"", City:"Melbourne"} 148 | // 149 | // Baidu Geocoding API 150 | // Melbourne VIC location is (31.227015, 121.456967) 151 | // Address of (-37.813611,144.963056) is 341 Little Bourke Street, Melbourne, Victoria, Australia 152 | // Detailed address: &geo.Address{FormattedAddress:"341 Little Bourke Street, Melbourne, Victoria, Australia", Street:"Little Bourke Street", HouseNumber:"341", Suburb:"", Postcode:"", State:"Victoria", StateCode:"", StateDistrict:"", County:"", Country:"Australia", CountryCode:"AUS", City:"Melbourne"} 153 | // 154 | // Amap Geocoding API 155 | // 北京市海淀区清河街道西二旗西路领秀新硅谷 location is (40.05510, 116.309866) 156 | // Address of (40.05510, 116.309866) is 北京市海淀区清河街道领秀新硅谷领秀新硅谷B区 157 | // Detailed address: &geo.Address{FormattedAddress:"北京市海淀区清河街道领秀新硅谷领秀新硅谷B区", Street:"西二旗西路", HouseNumber:"2号楼", Suburb:"海 淀区", Postcode:"", State:"北京市", StateCode:"", StateDistrict:"", County:"", Country:"中国", CountryCode:"", City:""} 158 | // 159 | // Mapbox API 160 | // Melbourne VIC location is (-37.814200, 144.963200) 161 | // Address of (-37.813611,144.963056) is Elwood Park Playground, Melbourne, Victoria 3000, Australia 162 | // Detailed address: &geo.Address{FormattedAddress:"Elwood Park Playground, Melbourne, Victoria 3000, Australia", 163 | // Street:"Elwood Park Playground", HouseNumber:"", Suburb:"", Postcode:"3000", State:"Victoria", StateDistrict:"", 164 | // County:"", Country:"Australia", CountryCode:"AU", City:"Melbourne"} 165 | // 166 | // OpenStreetMap 167 | // Melbourne VIC location is (-37.814217, 144.963161) 168 | // Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia 169 | // Detailed address: &geo.Address{FormattedAddress:"Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, 170 | // Victoria, 3000, Australia", Street:"Postal Lane", HouseNumber:"", Suburb:"Melbourne", Postcode:"3000", State:"Victoria", 171 | // StateDistrict:"", County:"", Country:"Australia", CountryCode:"AU", City:"Melbourne"} 172 | // 173 | // PickPoint 174 | // Melbourne VIC location is (-37.814217, 144.963161) 175 | // Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia 176 | // Detailed address: &geo.Address{FormattedAddress:"Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, 177 | // Victoria, 3000, Australia", Street:"Postal Lane", HouseNumber:"", Suburb:"Melbourne", Postcode:"3000", State:"Victoria", 178 | // StateDistrict:"", County:"", Country:"Australia", CountryCode:"AU", City:"Melbourne"} 179 | // 180 | // LocationIQ 181 | // Melbourne VIC location is (-37.814217, 144.963161) 182 | // Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia 183 | // Detailed address: &geo.Address{FormattedAddress:"Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, 184 | // Victoria, 3000, Australia", Street:"Postal Lane", HouseNumber:"", Suburb:"Melbourne", Postcode:"3000", State:"Victoria", 185 | // StateDistrict:"", County:"", Country:"Australia", CountryCode:"AU", City:"Melbourne"} 186 | // 187 | // ArcGIS 188 | // Melbourne VIC location is (-37.817530, 144.967150) 189 | // Address of (-37.813611,144.963056) is Melbourne's Gpo 190 | // Detailed address: &geo.Address{FormattedAddress:"Melbourne's Gpo", Street:"350 Bourke Street Mall", HouseNumber:"350", Suburb:"", Postcode:"3000", State:"Victoria", StateDistrict:"", County:"", Country:"", CountryCode:"AUS", City:""} 191 | // 192 | // geocod.io 193 | // Melbourne VIC location is (28.079357, -80.623618) 194 | // got address 195 | // 196 | // Mapzen 197 | // Melbourne VIC location is (45.551136, 11.533929) 198 | // Address of (-37.813611,144.963056) is Stop 3: Bourke Street Mall, Bourke Street, Melbourne, Australia 199 | // Detailed address: &geo.Address{FormattedAddress:"Stop 3: Bourke Street Mall, Bourke Street, Melbourne, Australia", Street:"", HouseNumber:"", Suburb:"", Postcode:"", State:"Victoria", StateDistrict:"", County:"", Country:"Australia", CountryCode:"AUS", City:""} 200 | // 201 | // TomTom 202 | // Melbourne VIC location is (-37.815340, 144.963230) 203 | // Address of (-37.813611,144.963056) is Doyles Road, Elaine, West Central Victoria, Victoria, 3334 204 | // Detailed address: &geo.Address{FormattedAddress:"Doyles Road, Elaine, West Central Victoria, Victoria, 3334", Street:"Doyles Road", HouseNumber:"", Suburb:"", Postcode:"3334", State:"Victoria", StateDistrict:"", County:"", Country:"Australia", CountryCode:"AU", City:"Elaine"} 205 | // 206 | // Yandex 207 | // Melbourne VIC location is (41.926823, 2.254232) 208 | // Address of (-37.813611,144.963056) is Victoria, City of Melbourne, Elizabeth Street 209 | // Detailed address: &geo.Address{FormattedAddress:"Victoria, City of Melbourne, Elizabeth Street", Street:"Elizabeth Street", 210 | // HouseNumber:"", Suburb:"", Postcode:"", State:"Victoria", StateDistrict:"", County:"", Country:"Australia", CountryCode:"AU", 211 | // City:"City of Melbourne"} 212 | // 213 | // FrenchAPIGouv 214 | // Champs de Mars Paris location is (2.304770, 48.854395) 215 | // Address of (48.854395,2.304770) is 9001, Parc du Champs de Mars, 75007, Paris, Paris, Île-de-France, France 216 | // Detailed address: &geo.Address{FormattedAddress:"9001, Parc du Champs de Mars, 75007, Paris, Paris, Île-de-France, France", 217 | // Street:"Parc du Champs de Mars", HouseNumber:"9001", Suburb:"", Postcode:"75007", State:" Île-de-France", StateDistrict:"", 218 | // County:" Paris", Country:"France", CountryCode:"FRA", City:"Paris"} 219 | // 220 | // ChainedAPI[OpenStreetmap -> Google] 221 | // Melbourne VIC location is (-37.814217, 144.963161) 222 | // Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia 223 | // Detailed address: &geo.Address{FormattedAddress:"Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, 224 | // Victoria, 3000, Australia", Street:"Postal Lane", HouseNumber:"", Suburb:"Melbourne", Postcode:"3000", State:"Victoria", 225 | // StateDistrict:"", County:"", Country:"Australia", CountryCode:"AU", City:"Melbourne"} 226 | } 227 | 228 | func try(geocoder geo.Geocoder) { 229 | location, _ := geocoder.Geocode(addr) 230 | if location != nil { 231 | fmt.Printf("%s location is (%.6f, %.6f)\n", addr, location.Lat, location.Lng) 232 | } else { 233 | fmt.Println("got location") 234 | } 235 | address, _ := geocoder.ReverseGeocode(lat, lng) 236 | if address != nil { 237 | fmt.Printf("Address of (%.6f,%.6f) is %s\n", lat, lng, address.FormattedAddress) 238 | fmt.Printf("Detailed address: %#v\n", address) 239 | } else { 240 | fmt.Println("got address") 241 | } 242 | fmt.Print("\n") 243 | } 244 | 245 | func tryOnlyForAmap(geocoder geo.Geocoder) { 246 | location, _ := geocoder.Geocode(addrAmap) 247 | if location != nil { 248 | fmt.Printf("%s location is (%.6f, %.6f)\n", addr, location.Lat, location.Lng) 249 | } else { 250 | fmt.Println("got location") 251 | } 252 | address, _ := geocoder.ReverseGeocode(latAmap, lngAmap) 253 | if address != nil { 254 | fmt.Printf("Address of (%.6f,%.6f) is %s\n", lat, lng, address.FormattedAddress) 255 | fmt.Printf("Detailed address: %#v\n", address) 256 | } else { 257 | fmt.Println("got address") 258 | } 259 | fmt.Print("\n") 260 | } 261 | 262 | func tryOnlyFRData(geocoder geo.Geocoder) { 263 | location, _ := geocoder.Geocode(addrFR) 264 | if location != nil { 265 | fmt.Printf("%s location is (%.6f, %.6f)\n", addrFR, location.Lat, location.Lng) 266 | } else { 267 | fmt.Println("got location") 268 | } 269 | address, _ := geocoder.ReverseGeocode(latFR, lngFR) 270 | if address != nil { 271 | fmt.Printf("Address of (%.6f,%.6f) is %s\n", latFR, lngFR, address.FormattedAddress) 272 | fmt.Printf("Detailed address: %#v\n", address) 273 | } else { 274 | fmt.Println("got address") 275 | } 276 | fmt.Print("\n") 277 | } 278 | -------------------------------------------------------------------------------- /bing/geocoder_test.go: -------------------------------------------------------------------------------- 1 | package bing_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/codingsince1985/geo-golang" 11 | "github.com/codingsince1985/geo-golang/bing" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | var key = os.Getenv("BING_API_KEY") 16 | 17 | func TestGeocode(t *testing.T) { 18 | ts := testServer(response1) 19 | defer ts.Close() 20 | 21 | geocoder := bing.Geocoder(key, ts.URL+"/") 22 | location, err := geocoder.Geocode("60 Collins St, Melbourne VIC") 23 | 24 | assert.NoError(t, err) 25 | assert.Equal(t, geo.Location{Lat: -37.81375, Lng: 144.97176}, *location) 26 | } 27 | 28 | func TestReverseGeocode(t *testing.T) { 29 | ts := testServer(response2) 30 | defer ts.Close() 31 | 32 | geocoder := bing.Geocoder(key, ts.URL+"/") 33 | address, err := geocoder.ReverseGeocode(-37.81375, 144.97176) 34 | 35 | assert.NoError(t, err) 36 | assert.True(t, strings.Index(address.FormattedAddress, "Collins St") > 0) 37 | } 38 | 39 | func TestReverseGeocodeWithNoResult(t *testing.T) { 40 | ts := testServer(response3) 41 | defer ts.Close() 42 | 43 | geocoder := bing.Geocoder(key, ts.URL+"/") 44 | addr, err := geocoder.ReverseGeocode(-37.81375, 164.97176) 45 | 46 | assert.NoError(t, err) 47 | assert.Nil(t, addr) 48 | } 49 | 50 | func testServer(response string) *httptest.Server { 51 | return httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 52 | resp.Write([]byte(response)) 53 | })) 54 | } 55 | 56 | const ( 57 | response1 = `{ 58 | "authenticationResultCode":"ValidCredentials", 59 | "brandLogoUri":"http:\/\/dev.virtualearth.net\/Branding\/logo_powered_by.png", 60 | "copyright":"Copyright © 2016 Microsoft and its suppliers. All rights reserved. This API cannot be accessed and the content and any results may not be used, reproduced or transmitted in any manner without express written permission from Microsoft Corporation.", 61 | "resourceSets":[ 62 | { 63 | "estimatedTotal":4, 64 | "resources":[ 65 | { 66 | "__type":"Location:http:\/\/schemas.microsoft.com\/search\/local\/ws\/rest\/v1", 67 | "bbox":[ 68 | -37.8148742, 69 | 144.970337, 70 | -37.8126258, 71 | 144.973183 72 | ], 73 | "name":"60 Collins St, Melbourne VIC 3000, Australia", 74 | "point":{ 75 | "type":"Point", 76 | "coordinates":[ 77 | -37.81375, 78 | 144.97176 79 | ] 80 | }, 81 | "address":{ 82 | "addressLine":"60 Collins St", 83 | "adminDistrict":"VIC", 84 | "countryRegion":"Australia", 85 | "formattedAddress":"60 Collins St, Melbourne VIC 3000, Australia", 86 | "locality":"Melbourne", 87 | "postalCode":"3000" 88 | }, 89 | "confidence":"Medium", 90 | "entityType":"Address", 91 | "geocodePoints":[ 92 | { 93 | "type":"Point", 94 | "coordinates":[ 95 | -37.81375, 96 | 144.97176 97 | ], 98 | "calculationMethod":"Rooftop", 99 | "usageTypes":[ 100 | "Display" 101 | ] 102 | }, 103 | { 104 | "type":"Point", 105 | "coordinates":[ 106 | -37.81393, 107 | 144.97185 108 | ], 109 | "calculationMethod":"Rooftop", 110 | "usageTypes":[ 111 | "Route" 112 | ] 113 | } 114 | ], 115 | "matchCodes":[ 116 | "Ambiguous", 117 | "Good" 118 | ] 119 | }, 120 | { 121 | "__type":"Location:http:\/\/schemas.microsoft.com\/search\/local\/ws\/rest\/v1", 122 | "bbox":[ 123 | -37.7563242, 124 | 145.0049782, 125 | -37.7540758, 126 | 145.0078218 127 | ], 128 | "name":"60 Collins St, Thornbury VIC 3071, Australia", 129 | "point":{ 130 | "type":"Point", 131 | "coordinates":[ 132 | -37.7552, 133 | 145.0064 134 | ] 135 | }, 136 | "address":{ 137 | "addressLine":"60 Collins St", 138 | "adminDistrict":"VIC", 139 | "countryRegion":"Australia", 140 | "formattedAddress":"60 Collins St, Thornbury VIC 3071, Australia", 141 | "locality":"Thornbury", 142 | "postalCode":"3071" 143 | }, 144 | "confidence":"Medium", 145 | "entityType":"Address", 146 | "geocodePoints":[ 147 | { 148 | "type":"Point", 149 | "coordinates":[ 150 | -37.7552, 151 | 145.0064 152 | ], 153 | "calculationMethod":"Rooftop", 154 | "usageTypes":[ 155 | "Display" 156 | ] 157 | }, 158 | { 159 | "type":"Point", 160 | "coordinates":[ 161 | -37.75505, 162 | 145.00642 163 | ], 164 | "calculationMethod":"Rooftop", 165 | "usageTypes":[ 166 | "Route" 167 | ] 168 | } 169 | ], 170 | "matchCodes":[ 171 | "Ambiguous", 172 | "Good" 173 | ] 174 | }, 175 | { 176 | "__type":"Location:http:\/\/schemas.microsoft.com\/search\/local\/ws\/rest\/v1", 177 | "bbox":[ 178 | -37.9809042, 179 | 145.0587838, 180 | -37.9786558, 181 | 145.0616362 182 | ], 183 | "name":"60 Collins St, Mentone VIC 3194, Australia", 184 | "point":{ 185 | "type":"Point", 186 | "coordinates":[ 187 | -37.97978, 188 | 145.06021 189 | ] 190 | }, 191 | "address":{ 192 | "addressLine":"60 Collins St", 193 | "adminDistrict":"VIC", 194 | "countryRegion":"Australia", 195 | "formattedAddress":"60 Collins St, Mentone VIC 3194, Australia", 196 | "locality":"Mentone", 197 | "postalCode":"3194" 198 | }, 199 | "confidence":"Medium", 200 | "entityType":"Address", 201 | "geocodePoints":[ 202 | { 203 | "type":"Point", 204 | "coordinates":[ 205 | -37.97978, 206 | 145.06021 207 | ], 208 | "calculationMethod":"Rooftop", 209 | "usageTypes":[ 210 | "Display" 211 | ] 212 | }, 213 | { 214 | "type":"Point", 215 | "coordinates":[ 216 | -37.97961, 217 | 145.06024 218 | ], 219 | "calculationMethod":"Rooftop", 220 | "usageTypes":[ 221 | "Route" 222 | ] 223 | } 224 | ], 225 | "matchCodes":[ 226 | "Ambiguous", 227 | "Good" 228 | ] 229 | }, 230 | { 231 | "__type":"Location:http:\/\/schemas.microsoft.com\/search\/local\/ws\/rest\/v1", 232 | "bbox":[ 233 | -37.7392142, 234 | 144.8016785, 235 | -37.7369658, 236 | 144.8045215 237 | ], 238 | "name":"60 Collins St, St Albans VIC 3021, Australia", 239 | "point":{ 240 | "type":"Point", 241 | "coordinates":[ 242 | -37.73809, 243 | 144.8031 244 | ] 245 | }, 246 | "address":{ 247 | "addressLine":"60 Collins St", 248 | "adminDistrict":"VIC", 249 | "countryRegion":"Australia", 250 | "formattedAddress":"60 Collins St, St Albans VIC 3021, Australia", 251 | "locality":"St Albans", 252 | "postalCode":"3021" 253 | }, 254 | "confidence":"Medium", 255 | "entityType":"Address", 256 | "geocodePoints":[ 257 | { 258 | "type":"Point", 259 | "coordinates":[ 260 | -37.73809, 261 | 144.8031 262 | ], 263 | "calculationMethod":"Rooftop", 264 | "usageTypes":[ 265 | "Display" 266 | ] 267 | }, 268 | { 269 | "type":"Point", 270 | "coordinates":[ 271 | -37.73807, 272 | 144.80292 273 | ], 274 | "calculationMethod":"Rooftop", 275 | "usageTypes":[ 276 | "Route" 277 | ] 278 | } 279 | ], 280 | "matchCodes":[ 281 | "Ambiguous", 282 | "Good" 283 | ] 284 | } 285 | ] 286 | } 287 | ], 288 | "statusCode":200, 289 | "statusDescription":"OK", 290 | "traceId":"f0ff69bb189c4fdba96a05a0ad86658c|HK20271556|02.00.164.2600|HK2SCH010280621, HK2SCH010281221, HK2SCH010281326, HK2SCH010260633, HK2SCH010310422, i-0186bca5.ap-southeast-1b" 291 | }` 292 | response2 = `{ 293 | "authenticationResultCode":"ValidCredentials", 294 | "brandLogoUri":"http:\/\/dev.virtualearth.net\/Branding\/logo_powered_by.png", 295 | "copyright":"Copyright © 2016 Microsoft and its suppliers. All rights reserved. This API cannot be accessed and the content and any results may not be used, reproduced or transmitted in any manner without express written permission from Microsoft Corporation.", 296 | "resourceSets":[ 297 | { 298 | "estimatedTotal":2, 299 | "resources":[ 300 | { 301 | "__type":"Location:http:\/\/schemas.microsoft.com\/search\/local\/ws\/rest\/v1", 302 | "bbox":[ 303 | -37.817612717570675, 304 | 144.96524105171127, 305 | -37.809887282429322, 306 | 144.9782789482887 307 | ], 308 | "name":"58 Collins St, Melbourne, VIC 3000", 309 | "point":{ 310 | "type":"Point", 311 | "coordinates":[ 312 | -37.81375, 313 | 144.97176 314 | ] 315 | }, 316 | "address":{ 317 | "addressLine":"58 Collins St", 318 | "adminDistrict":"VIC", 319 | "countryRegion":"Australia", 320 | "formattedAddress":"58 Collins St, Melbourne, VIC 3000", 321 | "locality":"Melbourne", 322 | "postalCode":"3000" 323 | }, 324 | "confidence":"Medium", 325 | "entityType":"Address", 326 | "geocodePoints":[ 327 | { 328 | "type":"Point", 329 | "coordinates":[ 330 | -37.81375, 331 | 144.97176 332 | ], 333 | "calculationMethod":"Interpolation", 334 | "usageTypes":[ 335 | "Display", 336 | "Route" 337 | ] 338 | } 339 | ], 340 | "matchCodes":[ 341 | "Ambiguous", 342 | "Good" 343 | ] 344 | }, 345 | { 346 | "__type":"Location:http:\/\/schemas.microsoft.com\/search\/local\/ws\/rest\/v1", 347 | "bbox":[ 348 | -37.817612717570675, 349 | 144.96524105171127, 350 | -37.809887282429322, 351 | 144.9782789482887 352 | ], 353 | "name":"Collins St, Melbourne, VIC 3000", 354 | "point":{ 355 | "type":"Point", 356 | "coordinates":[ 357 | -37.81375, 358 | 144.97176 359 | ] 360 | }, 361 | "address":{ 362 | "addressLine":"Collins St", 363 | "adminDistrict":"VIC", 364 | "countryRegion":"Australia", 365 | "formattedAddress":"Collins St, Melbourne, VIC 3000", 366 | "locality":"Melbourne", 367 | "postalCode":"3000" 368 | }, 369 | "confidence":"Medium", 370 | "entityType":"Address", 371 | "geocodePoints":[ 372 | { 373 | "type":"Point", 374 | "coordinates":[ 375 | -37.81375, 376 | 144.97176 377 | ], 378 | "calculationMethod":"Interpolation", 379 | "usageTypes":[ 380 | "Display", 381 | "Route" 382 | ] 383 | } 384 | ], 385 | "matchCodes":[ 386 | "Ambiguous", 387 | "Good" 388 | ] 389 | } 390 | ] 391 | } 392 | ], 393 | "statusCode":200, 394 | "statusDescription":"OK", 395 | "traceId":"2c056005fa6643a6ac6bb8a9bb2a3a5c|HK20271556|02.00.164.2600|HK2SCH010280621, HK2SCH010281619" 396 | }` 397 | response3 = `{ 398 | "authenticationResultCode":"ValidCredentials", 399 | "brandLogoUri":"http:\/\/dev.virtualearth.net\/Branding\/logo_powered_by.png", 400 | "copyright":"Copyright © 2016 Microsoft and its suppliers. All rights reserved. This API cannot be accessed and the content and any results may not be used, reproduced or transmitted in any manner without express written permission from Microsoft Corporation.", 401 | "resourceSets":[ 402 | { 403 | "estimatedTotal":0, 404 | "resources":[ 405 | 406 | ] 407 | } 408 | ], 409 | "statusCode":200, 410 | "statusDescription":"OK", 411 | "traceId":"101c3af9b0984cd0937b9e4dde2910a6|HK20271556|02.00.164.2600|HK2SCH010280621, HK2SCH010281619" 412 | }` 413 | ) 414 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GeoService in Go 2 | == 3 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/codingsince1985/geo-golang)](https://pkg.go.dev/github.com/codingsince1985/geo-golang) 4 | [![Build Status](https://travis-ci.org/codingsince1985/geo-golang.svg?branch=master)](https://travis-ci.org/codingsince1985/geo-golang) 5 | [![codecov](https://codecov.io/gh/codingsince1985/geo-golang/branch/master/graph/badge.svg)](https://codecov.io/gh/codingsince1985/geo-golang) 6 | [![Go Report Card](https://goreportcard.com/badge/codingsince1985/geo-golang)](https://goreportcard.com/report/codingsince1985/geo-golang) 7 | 8 | ## Code Coverage 9 | [![codecov.io](https://codecov.io/gh/codingsince1985/geo-golang/branch/master/graphs/icicle.svg)](https://codecov.io/gh/codingsince1985/geo-golang) 10 | 11 | A geocoding service developed in Go's way, idiomatic and elegant, not just in golang. 12 | 13 | This product is designed to open to any Geocoding service. Based on it, 14 | + [Google Maps](https://developers.google.com/maps/documentation/geocoding/) 15 | + MapQuest 16 | - [Nominatim Search](http://open.mapquestapi.com/nominatim/) 17 | - [Open Geocoding](http://open.mapquestapi.com/geocoding/) 18 | + [OpenCage](https://opencagedata.com/api) 19 | + HERE 20 | - [Geocoder API](https://developer.here.com/documentation/geocoder/dev_guide/topics/quick-start-geocode.html) 21 | - [Geocoding and Search API](https://developer.here.com/documentation/geocoding-search-api/dev_guide/index.html) 22 | + [Bing](https://msdn.microsoft.com/en-us/library/ff701715.aspx) 23 | + [Mapbox](https://www.mapbox.com/developers/api/geocoding/) 24 | + [OpenStreetMap](https://wiki.openstreetmap.org/wiki/Nominatim) 25 | + [PickPoint](https://pickpoint.io) 26 | + [LocationIQ](http://locationiq.org/) 27 | + [ArcGIS](https://www.arcgis.com/features/index.html) 28 | + [geocodio](https://geocod.io) 29 | + [Mapzen](https://mapzen.com) 30 | + [TomTom](https://www.tomtom.com) 31 | + [Yandex.Maps](https://tech.yandex.com/maps/doc/geocoder/desc/concepts/About-docpage/) 32 | + [French API Gouv](https://adresse.data.gouv.fr/api) 33 | + [Baidu Map](https://lbsyun.baidu.com/index.php?title=webapi/guide/webservice-geocoding) 34 | 35 | clients are implemented in ~50 LoC each. 36 | 37 | It allows you to switch from one service to another by changing only 1 line, or enjoy all the free quota (requests/sec, day, month...) from them at the same time. Just like this. 38 | 39 | ```go 40 | package main 41 | 42 | import ( 43 | "fmt" 44 | "os" 45 | 46 | "github.com/codingsince1985/geo-golang" 47 | "github.com/codingsince1985/geo-golang/arcgis" 48 | "github.com/codingsince1985/geo-golang/baidu" 49 | "github.com/codingsince1985/geo-golang/bing" 50 | "github.com/codingsince1985/geo-golang/chained" 51 | "github.com/codingsince1985/geo-golang/frenchapigouv" 52 | "github.com/codingsince1985/geo-golang/geocod" 53 | "github.com/codingsince1985/geo-golang/google" 54 | "github.com/codingsince1985/geo-golang/here" 55 | "github.com/codingsince1985/geo-golang/locationiq" 56 | "github.com/codingsince1985/geo-golang/mapbox" 57 | "github.com/codingsince1985/geo-golang/mapquest/nominatim" 58 | "github.com/codingsince1985/geo-golang/mapquest/open" 59 | "github.com/codingsince1985/geo-golang/mapzen" 60 | "github.com/codingsince1985/geo-golang/opencage" 61 | "github.com/codingsince1985/geo-golang/openstreetmap" 62 | "github.com/codingsince1985/geo-golang/pickpoint" 63 | "github.com/codingsince1985/geo-golang/tomtom" 64 | "github.com/codingsince1985/geo-golang/yandex" 65 | ) 66 | 67 | const ( 68 | addr = "Melbourne VIC" 69 | lat, lng = -37.813611, 144.963056 70 | radius = 50 71 | zoom = 18 72 | addrFR = "Champs de Mars Paris" 73 | latFR, lngFR = 48.854395, 2.304770 74 | ) 75 | 76 | func main() { 77 | ExampleGeocoder() 78 | } 79 | 80 | // ExampleGeocoder demonstrates the different geocoding services 81 | func ExampleGeocoder() { 82 | fmt.Println("Google Geocoding API") 83 | try(google.Geocoder(os.Getenv("GOOGLE_API_KEY"))) 84 | 85 | fmt.Println("Mapquest Nominatim") 86 | try(nominatim.Geocoder(os.Getenv("MAPQUEST_NOMINATIM_KEY"))) 87 | 88 | fmt.Println("Mapquest Open streetmaps") 89 | try(open.Geocoder(os.Getenv("MAPQUEST_OPEN_KEY"))) 90 | 91 | fmt.Println("OpenCage Data") 92 | try(opencage.Geocoder(os.Getenv("OPENCAGE_API_KEY"))) 93 | 94 | fmt.Println("HERE API") 95 | try(here.Geocoder(os.Getenv("HERE_APP_ID"), os.Getenv("HERE_APP_CODE"), radius)) 96 | 97 | fmt.Println("Bing Geocoding API") 98 | try(bing.Geocoder(os.Getenv("BING_API_KEY"))) 99 | 100 | fmt.Println("Baidu Geocoding API") 101 | try(baidu.Geocoder(os.Getenv("BAIDU_API_KEY"))) 102 | 103 | fmt.Println("Amap Geocoding API") 104 | try(amap.Geocoder(os.Getenv("BAIDU_API_KEY"))) 105 | 106 | fmt.Println("Mapbox API") 107 | try(mapbox.Geocoder(os.Getenv("MAPBOX_API_KEY"))) 108 | 109 | fmt.Println("OpenStreetMap") 110 | try(openstreetmap.Geocoder()) 111 | 112 | fmt.Println("PickPoint") 113 | try(pickpoint.Geocoder(os.Getenv("PICKPOINT_API_KEY"))) 114 | 115 | fmt.Println("LocationIQ") 116 | try(locationiq.Geocoder(os.Getenv("LOCATIONIQ_API_KEY"), zoom)) 117 | 118 | fmt.Println("ArcGIS") 119 | try(arcgis.Geocoder(os.Getenv("ARCGIS_TOKEN"))) 120 | 121 | fmt.Println("geocod.io") 122 | try(geocod.Geocoder(os.Getenv("GEOCOD_API_KEY"))) 123 | 124 | fmt.Println("Mapzen") 125 | try(mapzen.Geocoder(os.Getenv("MAPZEN_API_KEY"))) 126 | 127 | fmt.Println("TomTom") 128 | try(tomtom.Geocoder(os.Getenv("TOMTOM_API_KEY"))) 129 | 130 | fmt.Println("Yandex") 131 | try(yandex.Geocoder(os.Getenv("YANDEX_API_KEY"))) 132 | 133 | // To use only with french locations or addresses, 134 | // or values ​​could be estimated and will be false 135 | fmt.Println("FrenchAPIGouv") 136 | tryOnlyFRData(frenchapigouv.Geocoder()) 137 | 138 | // Chained geocoder will fallback to subsequent geocoders 139 | fmt.Println("ChainedAPI[OpenStreetmap -> Google]") 140 | try(chained.Geocoder( 141 | openstreetmap.Geocoder(), 142 | google.Geocoder(os.Getenv("GOOGLE_API_KEY")), 143 | )) 144 | } 145 | 146 | func try(geocoder geo.Geocoder) { 147 | location, _ := geocoder.Geocode(addr) 148 | if location != nil { 149 | fmt.Printf("%s location is (%.6f, %.6f)\n", addr, location.Lat, location.Lng) 150 | } else { 151 | fmt.Println("got location") 152 | } 153 | address, _ := geocoder.ReverseGeocode(lat, lng) 154 | if address != nil { 155 | fmt.Printf("Address of (%.6f,%.6f) is %s\n", lat, lng, address.FormattedAddress) 156 | fmt.Printf("Detailed address: %#v\n", address) 157 | } else { 158 | fmt.Println("got address") 159 | } 160 | fmt.Print("\n") 161 | } 162 | 163 | func tryOnlyFRData(geocoder geo.Geocoder) { 164 | location, _ := geocoder.Geocode(addrFR) 165 | if location != nil { 166 | fmt.Printf("%s location is (%.6f, %.6f)\n", addrFR, location.Lat, location.Lng) 167 | } else { 168 | fmt.Println("got location") 169 | } 170 | address, _ := geocoder.ReverseGeocode(latFR, lngFR) 171 | if address != nil { 172 | fmt.Printf("Address of (%.6f,%.6f) is %s\n", latFR, lngFR, address.FormattedAddress) 173 | fmt.Printf("Detailed address: %#v\n", address) 174 | } else { 175 | fmt.Println("got address") 176 | } 177 | fmt.Print("\n") 178 | } 179 | 180 | ``` 181 | ### Result 182 | ``` 183 | Google Geocoding API 184 | Melbourne VIC location is (-37.813611, 144.963056) 185 | Address of (-37.813611,144.963056) is 197 Elizabeth St, Melbourne VIC 3000, Australia 186 | Detailed address: &geo.Address{FormattedAddress:"197 Elizabeth St, Melbourne VIC 3000, Australia", Street:"Elizabeth Street", HouseNumber:"197", Suburb:"", Postcode:"3000", State:"Victoria", StateDistrict:"Melbourne City", County:"", Country:"Australia", CountryCode:"AU", City:"Melbourne"} 187 | 188 | Mapquest Nominatim 189 | Melbourne VIC location is (-37.814218, 144.963161) 190 | Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia 191 | Detailed address: &geo.Address{FormattedAddress:"Melbourne's GPO, Postal Lane, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia", Street:"Postal Lane", HouseNumber:"", Suburb:"Melbourne", Postcode:"3000", State:"Victoria", StateDistrict:"", County:"City of Melbourne", Country:"Australia", CountryCode:"AU", City:"Melbourne"} 192 | 193 | Mapquest Open streetmaps 194 | Melbourne VIC location is (-37.814218, 144.963161) 195 | Address of (-37.813611,144.963056) is Elizabeth Street, 3000, Melbourne, Victoria, AU 196 | Detailed address: &geo.Address{FormattedAddress:"Elizabeth Street, 3000, Melbourne, Victoria, AU", Street:"Elizabeth Street", HouseNumber:"", Suburb:"", Postcode:"3000", State:"Victoria", StateDistrict:"", County:"", Country:"", CountryCode:"AU", City:"Melbourne"} 197 | 198 | OpenCage Data 199 | Melbourne VIC location is (-37.814217, 144.963161) 200 | Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Melbourne VIC 3000, Australia 201 | Detailed address: &geo.Address{FormattedAddress:"Melbourne's GPO, Postal Lane, Melbourne VIC 3000, Australia", Street:"Postal Lane", HouseNumber:"", Suburb:"Melbourne (3000)", Postcode:"3000", State:"Victoria", StateDistrict:"", County:"City of Melbourne", Country:"Australia", CountryCode:"AU", City:"Melbourne"} 202 | 203 | HERE API 204 | Melbourne VIC location is (-37.817530, 144.967150) 205 | Address of (-37.813611,144.963056) is 197 Elizabeth St, Melbourne VIC 3000, Australia 206 | Detailed address: &geo.Address{FormattedAddress:"197 Elizabeth St, Melbourne VIC 3000, Australia", Street:"Elizabeth St", HouseNumber:"197", Suburb:"", Postcode:"3000", State:"Victoria", StateDistrict:"", County:"", Country:"Australia", CountryCode:"AUS", City:"Melbourne"} 207 | 208 | Bing Geocoding API 209 | Melbourne VIC location is (-37.824299, 144.977997) 210 | Address of (-37.813611,144.963056) is Elizabeth St, Melbourne, VIC 3000 211 | Detailed address: &geo.Address{FormattedAddress:"Elizabeth St, Melbourne, VIC 3000", Street:"Elizabeth St", HouseNumber:"", Suburb:"", Postcode:"3000", State:"", StateDistrict:"", County:"", Country:"Australia", CountryCode:"", City:"Melbourne"} 212 | 213 | Baidu Geocoding API 214 | Melbourne VIC location is (31.227015, 121.456967) 215 | Address of (-37.813611,144.963056) is 341 Little Bourke Street, Melbourne, Victoria, Australia 216 | Detailed address: &geo.Address{FormattedAddress:"341 Little Bourke Street, Melbourne, Victoria, Australia", Street:"Little Bourke Street", HouseNumber:"341", Suburb:"", Postcode:"", State:"Victoria", StateCode:"", StateDistrict:"", County:"", Country:"Australia", CountryCode:"AUS", City:"Melbourne"} 217 | 218 | Mapbox API 219 | Melbourne VIC location is (-37.814200, 144.963200) 220 | Address of (-37.813611,144.963056) is Elwood Park Playground, Melbourne, Victoria 3000, Australia 221 | Detailed address: &geo.Address{FormattedAddress:"Elwood Park Playground, Melbourne, Victoria 3000, Australia", Street:"Elwood Park Playground", HouseNumber:"", Suburb:"", Postcode:"3000", State:"Victoria", StateDistrict:"", County:"", Country:"Australia", CountryCode:"AU", City:"Melbourne"} 222 | 223 | OpenStreetMap 224 | Melbourne VIC location is (-37.814217, 144.963161) 225 | Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia 226 | Detailed address: &geo.Address{FormattedAddress:"Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia", Street:"Postal Lane", HouseNumber:"", Suburb:"Melbourne", Postcode:"3000", State:"Victoria", StateDistrict:"", County:"", Country:"Australia", CountryCode:"AU", City:"Melbourne"} 227 | 228 | PickPoint 229 | Melbourne VIC location is (-37.814217, 144.963161) 230 | Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia 231 | Detailed address: &geo.Address{FormattedAddress:"Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia", Street:"Postal Lane", HouseNumber:"", Suburb:"Melbourne", Postcode:"3000", State:"Victoria", StateDistrict:"", County:"", Country:"Australia", CountryCode:"AU", City:"Melbourne"} 232 | 233 | LocationIQ 234 | Melbourne VIC location is (-37.814217, 144.963161) 235 | Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia 236 | Detailed address: &geo.Address{FormattedAddress:"Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia", Street:"Postal Lane", HouseNumber:"", Suburb:"Melbourne", Postcode:"3000", State:"Victoria", StateDistrict:"", County:"", Country:"Australia", CountryCode:"AU", City:"Melbourne"} 237 | 238 | ArcGIS 239 | Melbourne VIC location is (-37.817530, 144.967150) 240 | Address of (-37.813611,144.963056) is Melbourne's Gpo 241 | Detailed address: &geo.Address{FormattedAddress:"Melbourne's Gpo", Street:"350 Bourke Street Mall", HouseNumber:"350", Suburb:"", Postcode:"3000", State:"Victoria", StateDistrict:"", County:"", Country:"", CountryCode:"AUS", City:""} 242 | 243 | geocod.io 244 | Melbourne VIC location is (28.079357, -80.623618) 245 | got address 246 | 247 | Mapzen 248 | Melbourne VIC location is (45.551136, 11.533929) 249 | Address of (-37.813611,144.963056) is Stop 3: Bourke Street Mall, Bourke Street, Melbourne, Australia 250 | Detailed address: &geo.Address{FormattedAddress:"Stop 3: Bourke Street Mall, Bourke Street, Melbourne, Australia", Street:"", HouseNumber:"", Suburb:"", Postcode:"", State:"Victoria", StateDistrict:"", County:"", Country:"Australia", CountryCode:"AUS", City:""} 251 | 252 | TomTom 253 | Melbourne VIC location is (-37.815340, 144.963230) 254 | Address of (-37.813611,144.963056) is Doyles Road, Elaine, West Central Victoria, Victoria, 3334 255 | Detailed address: &geo.Address{FormattedAddress:"Doyles Road, Elaine, West Central Victoria, Victoria, 3334", Street:"Doyles Road", HouseNumber:"", Suburb:"", Postcode:"3334", State:"Victoria", StateDistrict:"", County:"", Country:"Australia", CountryCode:"AU", City:"Elaine"} 256 | 257 | Yandex 258 | Melbourne VIC location is (41.926823, 2.254232) 259 | Address of (-37.813611,144.963056) is Victoria, City of Melbourne, Elizabeth Street 260 | Detailed address: &geo.Address{FormattedAddress:"Victoria, City of Melbourne, Elizabeth Street", Street:"Elizabeth Street", HouseNumber:"", Suburb:"", Postcode:"", State:"Victoria", StateDistrict:"", County:"", Country:"Australia", CountryCode:"AU", City:"City of Melbourne"} 261 | 262 | FrenchAPIGouv 263 | Champs de Mars Paris location is (2.304770, 48.854395) 264 | Address of (48.854395,2.304770) is 9001, Parc du Champs de Mars, 75007, Paris, Paris, Île-de-France, France 265 | Detailed address: &geo.Address{FormattedAddress:"9001, Parc du Champs de Mars, 75007, Paris, Paris, Île-de-France, France", Street:"Parc du Champs de Mars", HouseNumber:"9001", Suburb:"", Postcode:"75007", State:" Île-de-France", StateDistrict:"", County:" Paris", Country:"France", CountryCode:"FRA", City:"Paris"} 266 | 267 | ChainedAPI[OpenStreetmap -> Google] 268 | Melbourne VIC location is (-37.814217, 144.963161) 269 | Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia 270 | Detailed address: &geo.Address{FormattedAddress:"Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia", Street:"Postal Lane", HouseNumber:"", Suburb:"Melbourne", Postcode:"3000", State:"Victoria", StateDistrict:"", County:"", Country:"Australia", CountryCode:"AU", City:"Melbourne"} 271 | ``` 272 | License 273 | == 274 | geo-golang is distributed under the terms of the MIT license. See LICENSE for details. 275 | -------------------------------------------------------------------------------- /google/geocoder_test.go: -------------------------------------------------------------------------------- 1 | package google_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/codingsince1985/geo-golang" 11 | "github.com/codingsince1985/geo-golang/google" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | var token = os.Getenv("GOOGLE_API_KEY") 16 | 17 | func TestGeocode(t *testing.T) { 18 | ts := testServer(response1) 19 | defer ts.Close() 20 | 21 | geocoder := google.Geocoder(token, ts.URL+"/") 22 | location, err := geocoder.Geocode("60 Collins St, Melbourne VIC 3000") 23 | assert.NoError(t, err) 24 | assert.Equal(t, geo.Location{Lat: -37.8137683, Lng: 144.9718448}, *location) 25 | } 26 | 27 | func TestReverseGeocode(t *testing.T) { 28 | ts := testServer(response2) 29 | defer ts.Close() 30 | 31 | geocoder := google.Geocoder(token, ts.URL+"/") 32 | address, err := geocoder.ReverseGeocode(-37.8137683, 144.9718448) 33 | assert.NoError(t, err) 34 | assert.True(t, strings.HasPrefix(address.FormattedAddress, "60 Collins St")) 35 | assert.True(t, strings.HasPrefix(address.Street, "Collins St")) 36 | assert.Equal(t, address.StateCode, "VIC") 37 | } 38 | 39 | func TestReverseGeocodeWithNoResult(t *testing.T) { 40 | ts := testServer(response3) 41 | defer ts.Close() 42 | 43 | geocoder := google.Geocoder(token, ts.URL+"/") 44 | addr, err := geocoder.ReverseGeocode(-37.8137683, 164.9718448) 45 | assert.Nil(t, err) 46 | assert.Nil(t, addr) 47 | } 48 | 49 | func testServer(response string) *httptest.Server { 50 | return httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 51 | resp.Write([]byte(response)) 52 | })) 53 | } 54 | 55 | const ( 56 | response1 = `{ 57 | "results" : [ 58 | { 59 | "address_components" : [ 60 | { 61 | "long_name" : "60", 62 | "short_name" : "60", 63 | "types" : [ "street_number" ] 64 | }, 65 | { 66 | "long_name" : "Collins Street", 67 | "short_name" : "Collins St", 68 | "types" : [ "route" ] 69 | }, 70 | { 71 | "long_name" : "Melbourne", 72 | "short_name" : "Melbourne", 73 | "types" : [ "locality", "political" ] 74 | }, 75 | { 76 | "long_name" : "Victoria", 77 | "short_name" : "VIC", 78 | "types" : [ "administrative_area_level_1", "political" ] 79 | }, 80 | { 81 | "long_name" : "Australia", 82 | "short_name" : "AU", 83 | "types" : [ "country", "political" ] 84 | }, 85 | { 86 | "long_name" : "3000", 87 | "short_name" : "3000", 88 | "types" : [ "postal_code" ] 89 | } 90 | ], 91 | "formatted_address" : "60 Collins St, Melbourne VIC 3000, Australia", 92 | "geometry" : { 93 | "location" : { 94 | "lat" : -37.8137683, 95 | "lng" : 144.9718448 96 | }, 97 | "location_type" : "ROOFTOP", 98 | "viewport" : { 99 | "northeast" : { 100 | "lat" : -37.8124193197085, 101 | "lng" : 144.9731937802915 102 | }, 103 | "southwest" : { 104 | "lat" : -37.8151172802915, 105 | "lng" : 144.9704958197085 106 | } 107 | } 108 | }, 109 | "place_id" : "ChIJq9MCeshC1moRB2Z1U9DOwK0", 110 | "types" : [ "street_address" ] 111 | } 112 | ], 113 | "status" : "OK" 114 | }` 115 | response2 = `{ 116 | "results" : [ 117 | { 118 | "address_components" : [ 119 | { 120 | "long_name" : "60", 121 | "short_name" : "60", 122 | "types" : [ "street_number" ] 123 | }, 124 | { 125 | "long_name" : "Collins Street", 126 | "short_name" : "Collins St", 127 | "types" : [ "route" ] 128 | }, 129 | { 130 | "long_name" : "Melbourne", 131 | "short_name" : "Melbourne", 132 | "types" : [ "locality", "political" ] 133 | }, 134 | { 135 | "long_name" : "Victoria", 136 | "short_name" : "VIC", 137 | "types" : [ "administrative_area_level_1", "political" ] 138 | }, 139 | { 140 | "long_name" : "Australia", 141 | "short_name" : "AU", 142 | "types" : [ "country", "political" ] 143 | }, 144 | { 145 | "long_name" : "3000", 146 | "short_name" : "3000", 147 | "types" : [ "postal_code" ] 148 | } 149 | ], 150 | "formatted_address" : "60 Collins St, Melbourne VIC 3000, Australia", 151 | "geometry" : { 152 | "location" : { 153 | "lat" : -37.8137683, 154 | "lng" : 144.9718448 155 | }, 156 | "location_type" : "ROOFTOP", 157 | "viewport" : { 158 | "northeast" : { 159 | "lat" : -37.8124193197085, 160 | "lng" : 144.9731937802915 161 | }, 162 | "southwest" : { 163 | "lat" : -37.8151172802915, 164 | "lng" : 144.9704958197085 165 | } 166 | } 167 | }, 168 | "place_id" : "ChIJq9MCeshC1moRB2Z1U9DOwK0", 169 | "types" : [ "street_address" ] 170 | }, 171 | { 172 | "address_components" : [ 173 | { 174 | "long_name" : "Melbourne", 175 | "short_name" : "Melbourne", 176 | "types" : [ "locality", "political" ] 177 | }, 178 | { 179 | "long_name" : "Victoria", 180 | "short_name" : "VIC", 181 | "types" : [ "administrative_area_level_1", "political" ] 182 | }, 183 | { 184 | "long_name" : "Australia", 185 | "short_name" : "AU", 186 | "types" : [ "country", "political" ] 187 | } 188 | ], 189 | "formatted_address" : "Melbourne VIC, Australia", 190 | "geometry" : { 191 | "bounds" : { 192 | "northeast" : { 193 | "lat" : -37.7994893, 194 | "lng" : 144.9890618 195 | }, 196 | "southwest" : { 197 | "lat" : -37.8546255, 198 | "lng" : 144.9514222 199 | } 200 | }, 201 | "location" : { 202 | "lat" : -37.8142155, 203 | "lng" : 144.9632307 204 | }, 205 | "location_type" : "APPROXIMATE", 206 | "viewport" : { 207 | "northeast" : { 208 | "lat" : -37.7994893, 209 | "lng" : 144.9890618 210 | }, 211 | "southwest" : { 212 | "lat" : -37.8546255, 213 | "lng" : 144.9514222 214 | } 215 | } 216 | }, 217 | "place_id" : "ChIJgf0RD69C1moR4OeMIXVWBAU", 218 | "types" : [ "locality", "political" ] 219 | }, 220 | { 221 | "address_components" : [ 222 | { 223 | "long_name" : "Melbourne", 224 | "short_name" : "Melbourne", 225 | "types" : [ "colloquial_area", "locality", "political" ] 226 | }, 227 | { 228 | "long_name" : "Victoria", 229 | "short_name" : "VIC", 230 | "types" : [ "administrative_area_level_1", "political" ] 231 | }, 232 | { 233 | "long_name" : "Australia", 234 | "short_name" : "AU", 235 | "types" : [ "country", "political" ] 236 | } 237 | ], 238 | "formatted_address" : "Melbourne VIC, Australia", 239 | "geometry" : { 240 | "bounds" : { 241 | "northeast" : { 242 | "lat" : -37.4598457, 243 | "lng" : 145.76474 244 | }, 245 | "southwest" : { 246 | "lat" : -38.2607199, 247 | "lng" : 144.3944921 248 | } 249 | }, 250 | "location" : { 251 | "lat" : -37.814107, 252 | "lng" : 144.96328 253 | }, 254 | "location_type" : "APPROXIMATE", 255 | "viewport" : { 256 | "northeast" : { 257 | "lat" : -37.4598457, 258 | "lng" : 145.76474 259 | }, 260 | "southwest" : { 261 | "lat" : -38.2607199, 262 | "lng" : 144.3944921 263 | } 264 | } 265 | }, 266 | "place_id" : "ChIJ90260rVG1moRkM2MIXVWBAQ", 267 | "types" : [ "colloquial_area", "locality", "political" ] 268 | }, 269 | { 270 | "address_components" : [ 271 | { 272 | "long_name" : "3000", 273 | "short_name" : "3000", 274 | "types" : [ "postal_code" ] 275 | }, 276 | { 277 | "long_name" : "Melbourne", 278 | "short_name" : "Melbourne", 279 | "types" : [ "locality", "political" ] 280 | }, 281 | { 282 | "long_name" : "Victoria", 283 | "short_name" : "VIC", 284 | "types" : [ "administrative_area_level_1", "political" ] 285 | }, 286 | { 287 | "long_name" : "Australia", 288 | "short_name" : "AU", 289 | "types" : [ "country", "political" ] 290 | } 291 | ], 292 | "formatted_address" : "Melbourne VIC 3000, Australia", 293 | "geometry" : { 294 | "bounds" : { 295 | "northeast" : { 296 | "lat" : -37.7994893, 297 | "lng" : 144.9890618 298 | }, 299 | "southwest" : { 300 | "lat" : -37.8300559, 301 | "lng" : 144.9514222 302 | } 303 | }, 304 | "location" : { 305 | "lat" : -37.8152065, 306 | "lng" : 144.963937 307 | }, 308 | "location_type" : "APPROXIMATE", 309 | "viewport" : { 310 | "northeast" : { 311 | "lat" : -37.7994893, 312 | "lng" : 144.9890618 313 | }, 314 | "southwest" : { 315 | "lat" : -37.8300559, 316 | "lng" : 144.9514222 317 | } 318 | } 319 | }, 320 | "place_id" : "ChIJm7IcwrhC1moREDUuRnhWBBw", 321 | "types" : [ "postal_code" ] 322 | }, 323 | { 324 | "address_components" : [ 325 | { 326 | "long_name" : "CBD & South Melbourne", 327 | "short_name" : "CBD & South Melbourne", 328 | "types" : [ "political" ] 329 | }, 330 | { 331 | "long_name" : "Victoria", 332 | "short_name" : "VIC", 333 | "types" : [ "administrative_area_level_1", "political" ] 334 | }, 335 | { 336 | "long_name" : "Australia", 337 | "short_name" : "AU", 338 | "types" : [ "country", "political" ] 339 | } 340 | ], 341 | "formatted_address" : "CBD & South Melbourne, VIC, Australia", 342 | "geometry" : { 343 | "bounds" : { 344 | "northeast" : { 345 | "lat" : -37.7729624, 346 | "lng" : 145.0155638 347 | }, 348 | "southwest" : { 349 | "lat" : -37.8574994, 350 | "lng" : 144.8984946 351 | } 352 | }, 353 | "location" : { 354 | "lat" : -37.8362164, 355 | "lng" : 144.9501708 356 | }, 357 | "location_type" : "APPROXIMATE", 358 | "viewport" : { 359 | "northeast" : { 360 | "lat" : -37.7729624, 361 | "lng" : 145.0155638 362 | }, 363 | "southwest" : { 364 | "lat" : -37.8574994, 365 | "lng" : 144.8984946 366 | } 367 | } 368 | }, 369 | "place_id" : "ChIJORuuCkxd1moRNMrml7yk-C8", 370 | "types" : [ "political" ] 371 | }, 372 | { 373 | "address_components" : [ 374 | { 375 | "long_name" : "Victoria", 376 | "short_name" : "VIC", 377 | "types" : [ "administrative_area_level_1", "political" ] 378 | }, 379 | { 380 | "long_name" : "Australia", 381 | "short_name" : "AU", 382 | "types" : [ "country", "political" ] 383 | } 384 | ], 385 | "formatted_address" : "Victoria, Australia", 386 | "geometry" : { 387 | "bounds" : { 388 | "northeast" : { 389 | "lat" : -33.9810507, 390 | "lng" : 149.9764882 391 | }, 392 | "southwest" : { 393 | "lat" : -39.2247306, 394 | "lng" : 140.9624773 395 | } 396 | }, 397 | "location" : { 398 | "lat" : -37.4713077, 399 | "lng" : 144.7851531 400 | }, 401 | "location_type" : "APPROXIMATE", 402 | "viewport" : { 403 | "northeast" : { 404 | "lat" : -33.9810507, 405 | "lng" : 149.9764882 406 | }, 407 | "southwest" : { 408 | "lat" : -39.2247306, 409 | "lng" : 140.9624773 410 | } 411 | } 412 | }, 413 | "place_id" : "ChIJT5UYfksx1GoRNJWCvuL8Tlo", 414 | "types" : [ "administrative_area_level_1", "political" ] 415 | }, 416 | { 417 | "address_components" : [ 418 | { 419 | "long_name" : "Australia", 420 | "short_name" : "AU", 421 | "types" : [ "country", "political" ] 422 | } 423 | ], 424 | "formatted_address" : "Australia", 425 | "geometry" : { 426 | "bounds" : { 427 | "northeast" : { 428 | "lat" : -9.2198215, 429 | "lng" : 159.255497 430 | }, 431 | "southwest" : { 432 | "lat" : -54.7772185, 433 | "lng" : 112.9214336 434 | } 435 | }, 436 | "location" : { 437 | "lat" : -25.274398, 438 | "lng" : 133.775136 439 | }, 440 | "location_type" : "APPROXIMATE", 441 | "viewport" : { 442 | "northeast" : { 443 | "lat" : -0.6911343999999999, 444 | "lng" : 166.7429167 445 | }, 446 | "southwest" : { 447 | "lat" : -51.66332320000001, 448 | "lng" : 100.0911072 449 | } 450 | } 451 | }, 452 | "place_id" : "ChIJ38WHZwf9KysRUhNblaFnglM", 453 | "types" : [ "country", "political" ] 454 | } 455 | ], 456 | "status" : "OK" 457 | }` 458 | response3 = `{ 459 | "results" : [], 460 | "status" : "ZERO_RESULTS" 461 | }` 462 | ) 463 | -------------------------------------------------------------------------------- /opencage/geocoder_test.go: -------------------------------------------------------------------------------- 1 | package opencage_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/codingsince1985/geo-golang/opencage" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | var key = os.Getenv("OPENCAGE_API_KEY") 15 | 16 | // locDelta is the lat long precision significant digits we care about 17 | // because the opencage API isn't the same exact lat,long as other systems 18 | var locDelta = 0.01 19 | 20 | func TestGeocode(t *testing.T) { 21 | ts := testServer(response1) 22 | defer ts.Close() 23 | 24 | geocoder := opencage.Geocoder(key, ts.URL+"/") 25 | location, err := geocoder.Geocode("60 Collins St, Melbourne VIC 3000") 26 | assert.Nil(t, err) 27 | assert.InDelta(t, -37.8154176, location.Lat, locDelta) 28 | assert.InDelta(t, 144.9665563, location.Lng, locDelta) 29 | } 30 | 31 | func TestReverseGeocode(t *testing.T) { 32 | ts := testServer(response2) 33 | defer ts.Close() 34 | 35 | geocoder := opencage.Geocoder(key, ts.URL+"/") 36 | address, err := geocoder.ReverseGeocode(-37.8154176, 144.9665563) 37 | assert.NoError(t, err) 38 | assert.True(t, strings.Index(address.FormattedAddress, "Collins St") > 0) 39 | } 40 | 41 | func TestReverseGeocodeWithNoResult(t *testing.T) { 42 | ts := testServer(response3) 43 | defer ts.Close() 44 | 45 | geocoder := opencage.Geocoder(key, ts.URL+"/") 46 | address, err := geocoder.ReverseGeocode(-37.8154176, 164.9665563) 47 | assert.Nil(t, err) 48 | assert.Nil(t, address) 49 | } 50 | 51 | func TestReverseGeocodeUsingSuburbAsLocality(t *testing.T) { 52 | ts := testServer(responseMissingLocality) 53 | defer ts.Close() 54 | 55 | geocoder := opencage.Geocoder(key, ts.URL+"/") 56 | address, err := geocoder.ReverseGeocode(-37.8154176, 164.9665563) 57 | assert.Nil(t, err) 58 | assert.NotNil(t, address) 59 | assert.Equal(t, "Lütten Klein", address.City) 60 | } 61 | 62 | func testServer(response string) *httptest.Server { 63 | return httptest.NewServer(http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 64 | resp.Write([]byte(response)) 65 | })) 66 | } 67 | 68 | const ( 69 | response1 = `{ 70 | "documentation":"https://geocoder.opencagedata.com/api", 71 | "licenses":[ 72 | { 73 | "name":"CC-BY-SA", 74 | "url":"http://creativecommons.org/licenses/by-sa/3.0/" 75 | }, 76 | { 77 | "name":"ODbL", 78 | "url":"http://opendatacommons.org/licenses/odbl/summary/" 79 | } 80 | ], 81 | "rate":{ 82 | "limit":2500, 83 | "remaining":2383, 84 | "reset":1463184000 85 | }, 86 | "results":[ 87 | { 88 | "annotations":{ 89 | "DMS":{ 90 | "lat":"37\u00b0 48' 59.45148'' S", 91 | "lng":"144\u00b0 57' 47.23560'' E" 92 | }, 93 | "MGRS":"55HCU2071312588", 94 | "Maidenhead":"QF22le54na", 95 | "Mercator":{ 96 | "x":16137220.814, 97 | "y":-4527336.343 98 | }, 99 | "OSM":{ 100 | "edit_url":"https://www.openstreetmap.org/edit?way=215320282#map=17/-37.81651/144.96312", 101 | "url":"https://www.openstreetmap.org/?mlat=-37.81651&mlon=144.96312#map=17/-37.81651/144.96312" 102 | }, 103 | "callingcode":61, 104 | "geohash":"r1r0fewyvdc3r425n4nx", 105 | "sun":{ 106 | "rise":{ 107 | "apparent":1463173980, 108 | "astronomical":1463168580, 109 | "civil":1463172300, 110 | "nautical":1463170440 111 | }, 112 | "set":{ 113 | "apparent":1463124120, 114 | "astronomical":1463129580, 115 | "civil":1463125800, 116 | "nautical":1463127720 117 | } 118 | }, 119 | "timezone":{ 120 | "name":"Australia/Melbourne", 121 | "now_in_dst":0, 122 | "offset_sec":36000, 123 | "offset_string":1000, 124 | "short_name":"AEST" 125 | }, 126 | "what3words":{ 127 | "words":"themes.fees.tolls" 128 | } 129 | }, 130 | "bounds":{ 131 | "northeast":{ 132 | "lat":-37.8162553, 133 | "lng":144.9640149 134 | }, 135 | "southwest":{ 136 | "lat":-37.8169249, 137 | "lng":144.9617036 138 | } 139 | }, 140 | "components":{ 141 | "_type":"road", 142 | "city":"Melbourne", 143 | "country":"Australia", 144 | "country_code":"au", 145 | "county":"City of Melbourne", 146 | "postcode":"3000", 147 | "region":"Greater Melbourne", 148 | "road":"Collins Street", 149 | "state":"Victoria", 150 | "suburb":"Melbourne" 151 | }, 152 | "confidence":10, 153 | "formatted":"Collins Street, Melbourne VIC 3000, Australia", 154 | "geometry":{ 155 | "lat":-37.8165143, 156 | "lng":144.963121 157 | } 158 | }, 159 | { 160 | "annotations":{ 161 | "DMS":{ 162 | "lat":"37\u00b0 48' 50.40000'' S", 163 | "lng":"144\u00b0 57' 47.88000'' E" 164 | }, 165 | "MGRS":"55HCU2072312867", 166 | "Maidenhead":"QF22le54op", 167 | "Mercator":{ 168 | "x":16137240.74, 169 | "y":-4526983.531 170 | }, 171 | "OSM":{ 172 | "url":"https://www.openstreetmap.org/?mlat=-37.81400&mlon=144.96330#map=17/-37.81400/144.96330" 173 | }, 174 | "geohash":"r1r0fspj3xqzwneegpsn", 175 | "sun":{ 176 | "rise":{ 177 | "apparent":1463173980, 178 | "astronomical":1463168580, 179 | "civil":1463172300, 180 | "nautical":1463170440 181 | }, 182 | "set":{ 183 | "apparent":1463124120, 184 | "astronomical":1463129580, 185 | "civil":1463125800, 186 | "nautical":1463127720 187 | } 188 | }, 189 | "timezone":{ 190 | "name":"Australia/Melbourne", 191 | "now_in_dst":0, 192 | "offset_sec":36000, 193 | "offset_string":1000, 194 | "short_name":"AEST" 195 | }, 196 | "what3words":{ 197 | "words":"chat.seat.fortunate" 198 | } 199 | }, 200 | "components":{ 201 | "_type":"postcode", 202 | "country":"Australia", 203 | "postcode":"3000" 204 | }, 205 | "confidence":10, 206 | "formatted":"3000, Australia", 207 | "geometry":{ 208 | "lat":-37.814, 209 | "lng":144.9633 210 | } 211 | } 212 | ], 213 | "status":{ 214 | "code":200, 215 | "message":"OK" 216 | }, 217 | "stay_informed":{ 218 | "blog":"http://blog.opencagedata.com", 219 | "twitter":"https://twitter.com/opencagedata" 220 | }, 221 | "thanks":"For using an OpenCage Data API", 222 | "timestamp":{ 223 | "created_http":"Fri, 13 May 2016 10:39:20 GMT", 224 | "created_unix":1463135960 225 | }, 226 | "total_results":2 227 | }` 228 | response2 = `{ 229 | "documentation":"https://geocoder.opencagedata.com/api", 230 | "licenses":[ 231 | { 232 | "name":"CC-BY-SA", 233 | "url":"http://creativecommons.org/licenses/by-sa/3.0/" 234 | }, 235 | { 236 | "name":"ODbL", 237 | "url":"http://opendatacommons.org/licenses/odbl/summary/" 238 | } 239 | ], 240 | "rate":{ 241 | "limit":2500, 242 | "remaining":2382, 243 | "reset":1463184000 244 | }, 245 | "results":[ 246 | { 247 | "annotations":{ 248 | "DMS":{ 249 | "lat":"37\u00b0 48' 55.11266'' S", 250 | "lng":"144\u00b0 57' 59.36892'' E" 251 | }, 252 | "MGRS":"55HCU2100712728", 253 | "Maidenhead":"QF22le54xh", 254 | "Mercator":{ 255 | "x":16137596.001, 256 | "y":-4527167.222 257 | }, 258 | "OSM":{ 259 | "edit_url":"https://www.openstreetmap.org/edit?way=87337974#map=17/-37.81531/144.96649", 260 | "url":"https://www.openstreetmap.org/?mlat=-37.81531&mlon=144.96649#map=17/-37.81531/144.96649" 261 | }, 262 | "callingcode":61, 263 | "geohash":"r1r0fgcmu56wg15ek296", 264 | "sun":{ 265 | "rise":{ 266 | "apparent":1463173980, 267 | "astronomical":1463168580, 268 | "civil":1463172300, 269 | "nautical":1463170440 270 | }, 271 | "set":{ 272 | "apparent":1463124120, 273 | "astronomical":1463129580, 274 | "civil":1463125800, 275 | "nautical":1463127720 276 | } 277 | }, 278 | "timezone":{ 279 | "name":"Australia/Melbourne", 280 | "now_in_dst":0, 281 | "offset_sec":36000, 282 | "offset_string":1000, 283 | "short_name":"AEST" 284 | }, 285 | "what3words":{ 286 | "words":"clocks.beard.slices" 287 | } 288 | }, 289 | "components":{ 290 | "_type":"building", 291 | "city":"Melbourne", 292 | "country":"Australia", 293 | "country_code":"au", 294 | "county":"City of Melbourne", 295 | "house_number":"230", 296 | "postcode":"3000", 297 | "region":"Greater Melbourne", 298 | "road":"Collins Street", 299 | "state":"Victoria", 300 | "suburb":"Melbourne" 301 | }, 302 | "confidence":10, 303 | "formatted":"230 Collins Street, Melbourne VIC 3000, Australia", 304 | "geometry":{ 305 | "lat":-37.8153091, 306 | "lng":144.9664914 307 | } 308 | } 309 | ], 310 | "status":{ 311 | "code":200, 312 | "message":"OK" 313 | }, 314 | "stay_informed":{ 315 | "blog":"http://blog.opencagedata.com", 316 | "twitter":"https://twitter.com/opencagedata" 317 | }, 318 | "thanks":"For using an OpenCage Data API", 319 | "timestamp":{ 320 | "created_http":"Fri, 13 May 2016 10:43:19 GMT", 321 | "created_unix":1463136199 322 | }, 323 | "total_results":1 324 | }` 325 | response3 = `{ 326 | "documentation":"https://geocoder.opencagedata.com/api", 327 | "licenses":[ 328 | { 329 | "name":"CC-BY-SA", 330 | "url":"http://creativecommons.org/licenses/by-sa/3.0/" 331 | }, 332 | { 333 | "name":"ODbL", 334 | "url":"http://opendatacommons.org/licenses/odbl/summary/" 335 | } 336 | ], 337 | "rate":{ 338 | "limit":2500, 339 | "remaining":2381, 340 | "reset":1463184000 341 | }, 342 | "results":[ 343 | 344 | ], 345 | "status":{ 346 | "code":200, 347 | "message":"OK" 348 | }, 349 | "stay_informed":{ 350 | "blog":"http://blog.opencagedata.com", 351 | "twitter":"https://twitter.com/opencagedata" 352 | }, 353 | "thanks":"For using an OpenCage Data API", 354 | "timestamp":{ 355 | "created_http":"Fri, 13 May 2016 10:34:05 GMT", 356 | "created_unix":1463135645 357 | }, 358 | "total_results":0 359 | }` 360 | responseMissingLocality = `{ 361 | "documentation": "https://opencagedata.com/api", 362 | "licenses": [ 363 | { 364 | "name": "CC-BY-SA", 365 | "url": "https://creativecommons.org/licenses/by-sa/3.0/" 366 | }, 367 | { 368 | "name": "ODbL", 369 | "url": "https://opendatacommons.org/licenses/odbl/summary/" 370 | } 371 | ], 372 | "results": [ 373 | { 374 | "annotations": { 375 | "DMS": { 376 | "lat": "54° 8' 30.29676'' N", 377 | "lng": "12° 3' 0.72252'' E" 378 | }, 379 | "MGRS": "33UUA0732603314", 380 | "Maidenhead": "JO64ad64aa", 381 | "Mercator": { 382 | "x": 1341422.206, 383 | "y": 7162391.731 384 | }, 385 | "OSM": { 386 | "edit_url": "https://www.openstreetmap.org/edit?way=89311416#map=17/54.14175/12.05020", 387 | "url": "https://www.openstreetmap.org/?mlat=54.14175&mlon=12.05020#map=17/54.14175/12.05020" 388 | }, 389 | "callingcode": 49, 390 | "currency": { 391 | "alternate_symbols": [], 392 | "decimal_mark": ",", 393 | "html_entity": "€", 394 | "iso_code": "EUR", 395 | "iso_numeric": 978, 396 | "name": "Euro", 397 | "smallest_denomination": 1, 398 | "subunit": "Cent", 399 | "subunit_to_unit": 100, 400 | "symbol": "€", 401 | "symbol_first": 1, 402 | "thousands_separator": "." 403 | }, 404 | "flag": "\ud83c\udde9\ud83c\uddea🇩🇪", 405 | "geohash": "u38s40nww18cs2tdhe9q", 406 | "qibla": 136.28, 407 | "sun": { 408 | "rise": { 409 | "apparent": 1542177540, 410 | "astronomical": 1542170040, 411 | "civil": 1542175140, 412 | "nautical": 1542172560 413 | }, 414 | "set": { 415 | "apparent": 1542208380, 416 | "astronomical": 1542215880, 417 | "civil": 1542210780, 418 | "nautical": 1542213360 419 | } 420 | }, 421 | "timezone": { 422 | "name": "Europe/Berlin", 423 | "now_in_dst": 0, 424 | "offset_sec": 3600, 425 | "offset_string": 100, 426 | "short_name": "CET" 427 | }, 428 | "what3words": { 429 | "words": "staked.nimbly.scatter" 430 | } 431 | }, 432 | "bounds": { 433 | "northeast": { 434 | "lat": 54.1418491, 435 | "lng": 12.0503007 436 | }, 437 | "southwest": { 438 | "lat": 54.1416491, 439 | "lng": 12.0501007 440 | } 441 | }, 442 | "components": { 443 | "ISO_3166-1_alpha-2": "DE", 444 | "_type": "building", 445 | "city_district": "Ortsbeirat 5 : Lütten Klein", 446 | "country": "Germany", 447 | "country_code": "de", 448 | "county": "Rostock", 449 | "house_number": "30", 450 | "political_union": "European Union", 451 | "postcode": "18107", 452 | "road": "Turkuer Straße", 453 | "state": "Mecklenburg-Vorpommern", 454 | "suburb": "Lütten Klein" 455 | }, 456 | "confidence": 10, 457 | "formatted": "Turkuer Straße 30, 18107 Rostock, Germany", 458 | "geometry": { 459 | "lat": 54.1417491, 460 | "lng": 12.0502007 461 | } 462 | } 463 | ], 464 | "status": { 465 | "code": 200, 466 | "message": "OK" 467 | }, 468 | "stay_informed": { 469 | "blog": "https://blog.opencagedata.com", 470 | "twitter": "https://twitter.com/opencagedata" 471 | }, 472 | "thanks": "For using an OpenCage Data API", 473 | "timestamp": { 474 | "created_http": "Wed, 14 Nov 2018 11:55:11 GMT", 475 | "created_unix": 1542196511 476 | }, 477 | "total_results": 1 478 | }` 479 | ) 480 | --------------------------------------------------------------------------------