-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnominatim.go
More file actions
143 lines (127 loc) · 4.09 KB
/
Copy pathnominatim.go
File metadata and controls
143 lines (127 loc) · 4.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package main
import (
"strconv"
"strings"
"github.com/authenticvision/rgeo"
)
// nominatimAddress mirrors the subset of Nominatim's "address" object that
// geocoder-php/nominatim-provider (used by Lychee) reads. Fields we can't
// populate from rgeo's country/province/city data (road, house_number,
// postcode, suburb, ...) are simply omitted.
type nominatimAddress struct {
City string `json:"city,omitempty"`
State string `json:"state,omitempty"`
Country string `json:"country,omitempty"`
CountryCode string `json:"country_code,omitempty"`
}
// nominatimReverseResponse mirrors the fields of a Nominatim /reverse
// jsonv2 response that geocoder-php/nominatim-provider reads.
type nominatimReverseResponse struct {
PlaceID int64 `json:"place_id"`
Licence string `json:"licence"`
Lat string `json:"lat"`
Lon string `json:"lon"`
DisplayName string `json:"display_name"`
Address nominatimAddress `json:"address"`
BoundingBox [4]string `json:"boundingbox"`
}
type nominatimErrorResponse struct {
Error string `json:"error"`
}
const licenceNotice = "Data from naturalearthdata.com (public domain), reverse geocoded locally by rgeo. Not affiliated with or sourced from OpenStreetMap."
// buildDisplayName joins the resolved parts from most to least specific,
// matching Nominatim's display_name convention.
func buildDisplayName(city, province, country string) string {
parts := make([]string, 0, 3)
for _, p := range []string{city, province, country} {
if p != "" {
parts = append(parts, p)
}
}
return strings.Join(parts, ", ")
}
// placeID synthesizes a stable, deterministic id from the resolved location
// so repeated lookups of the same place return the same value. Nothing in
// the Lychee/geocoder-php call path relies on this beyond it being present.
func placeID(country, province, city string) int64 {
h := int64(2166136261)
for _, r := range country + "|" + province + "|" + city {
h = (h ^ int64(r)) * 16777619
}
if h < 0 {
h = -h
}
return h
}
// boundingBox synthesizes an approximate box around the query point since
// rgeo doesn't expose the matched polygon's extent through its public API.
// marginDeg should roughly track the snapping distance used for the lookup.
func boundingBox(lat, lon, marginDeg float64) [4]string {
south := clampLat(lat - marginDeg)
north := clampLat(lat + marginDeg)
west := clampLon(lon - marginDeg)
east := clampLon(lon + marginDeg)
return [4]string{
formatCoord(south),
formatCoord(north),
formatCoord(west),
formatCoord(east),
}
}
func clampLat(v float64) float64 {
if v < -90 {
return -90
}
if v > 90 {
return 90
}
return v
}
func clampLon(v float64) float64 {
if v < -180 {
return -180
}
if v > 180 {
return 180
}
return v
}
func formatCoord(v float64) string {
return strconv.FormatFloat(v, 'f', 7, 64)
}
// zoomTrimCity/zoomTrimProvince are the minimum "zoom" (per Nominatim's
// /reverse semantics, see nominatim.org/release-docs/latest/api/Reverse)
// below which the corresponding detail level is dropped from the response.
// rgeo never resolves anything finer than city, so zoom can only ever trim
// detail here, never add it.
const (
zoomTrimProvince = 6
zoomTrimCity = 10
)
// buildReverseResponse assembles a Nominatim-jsonv2-compatible /reverse
// response from a resolved rgeo Location and the query's lat/lon/zoom.
func buildReverseResponse(loc rgeo.Location, lat, lon float64, zoom int) nominatimReverseResponse {
city := loc.City
province := loc.Province
if zoom < zoomTrimCity {
city = ""
}
if zoom < zoomTrimProvince {
province = ""
}
addr := nominatimAddress{
City: city,
State: province,
Country: loc.Country,
CountryCode: toLower(loc.CountryCode2),
}
return nominatimReverseResponse{
PlaceID: placeID(loc.Country, province, city),
Licence: licenceNotice,
Lat: formatCoord(lat),
Lon: formatCoord(lon),
DisplayName: buildDisplayName(city, province, loc.Country),
Address: addr,
BoundingBox: boundingBox(lat, lon, snappingDistanceKM/111.0),
}
}