226 lines
5.7 KiB
Go
226 lines
5.7 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// Address points represent mapped address nodes or the mean of a way's vertices.
|
|
type addressPoint struct {
|
|
Lat float64 `json:"lat"`
|
|
Lon float64 `json:"lon"`
|
|
Label string `json:"label"`
|
|
}
|
|
type addressCell struct{ lat, lon int }
|
|
type addressIndex struct {
|
|
cells map[addressCell][]addressPoint
|
|
count int
|
|
}
|
|
|
|
const localAddressRange = 5000.0
|
|
|
|
func cellFor(lat, lon float64) addressCell {
|
|
return addressCell{int(math.Floor(lat * 100)), int(math.Floor(lon * 100))}
|
|
}
|
|
func validCoordinate(lat, lon float64) bool {
|
|
return !math.IsNaN(lat) && !math.IsNaN(lon) && !math.IsInf(lat, 0) && !math.IsInf(lon, 0) && math.Abs(lat) <= 90 && math.Abs(lon) <= 180
|
|
}
|
|
func loadAddresses(path string) (*addressIndex, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
var points []addressPoint
|
|
if err = json.NewDecoder(f).Decode(&points); err != nil {
|
|
return nil, fmt.Errorf("read address data: %w", err)
|
|
}
|
|
idx := &addressIndex{cells: make(map[addressCell][]addressPoint)}
|
|
for _, p := range points {
|
|
if !validCoordinate(p.Lat, p.Lon) || strings.TrimSpace(p.Label) == "" {
|
|
return nil, fmt.Errorf("invalid address record")
|
|
}
|
|
cell := cellFor(p.Lat, p.Lon)
|
|
idx.cells[cell] = append(idx.cells[cell], p)
|
|
idx.count++
|
|
}
|
|
if idx.count == 0 {
|
|
return nil, fmt.Errorf("address data contains no addresses")
|
|
}
|
|
return idx, nil
|
|
}
|
|
func (idx *addressIndex) view(lat, lon float64) addressResult {
|
|
if !validCoordinate(lat, lon) {
|
|
return addressResult{Status: "unavailable"}
|
|
}
|
|
// An oversized geographic bounding box encloses the 5 km search circle.
|
|
// This index is intended for Mississippi, away from the poles/dateline.
|
|
dlat := localAddressRange / 110000
|
|
dlon := dlat / math.Max(0.01, math.Cos((math.Abs(lat)+dlat)*math.Pi/180))
|
|
lo, hi := cellFor(lat-dlat, lon-dlon), cellFor(lat+dlat, lon+dlon)
|
|
best := localAddressRange
|
|
var found *addressPoint
|
|
for y := lo.lat; y <= hi.lat; y++ {
|
|
for x := lo.lon; x <= hi.lon; x++ {
|
|
for _, p := range idx.cells[addressCell{y, x}] {
|
|
distance := distanceMeters(lat, lon, p.Lat, p.Lon)
|
|
if distance <= best {
|
|
copy := p
|
|
found = ©
|
|
best = distance
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if found == nil {
|
|
return addressResult{Status: "unavailable"}
|
|
}
|
|
return addressResult{Status: "ready", Address: &nearbyAddress{Label: found.Label, DistanceMeters: best, Attribution: "© OpenStreetMap contributors · local address data"}}
|
|
}
|
|
|
|
type osmTag struct {
|
|
Key string `xml:"k,attr"`
|
|
Value string `xml:"v,attr"`
|
|
}
|
|
type osmNode struct {
|
|
ID int64 `xml:"id,attr"`
|
|
Lat float64 `xml:"lat,attr"`
|
|
Lon float64 `xml:"lon,attr"`
|
|
Tags []osmTag `xml:"tag"`
|
|
}
|
|
type osmWay struct {
|
|
Refs []struct {
|
|
ID int64 `xml:"ref,attr"`
|
|
} `xml:"nd"`
|
|
Tags []osmTag `xml:"tag"`
|
|
}
|
|
|
|
func addressLabel(tags []osmTag) string {
|
|
values := make(map[string]string)
|
|
for _, tag := range tags {
|
|
values[tag.Key] = strings.TrimSpace(tag.Value)
|
|
}
|
|
street := values["addr:street"]
|
|
if street == "" {
|
|
street = values["addr:place"]
|
|
}
|
|
if values["addr:housenumber"] == "" || street == "" {
|
|
return ""
|
|
}
|
|
parts := []string{values["addr:housenumber"] + " " + street}
|
|
for _, k := range []string{"addr:unit", "addr:city", "addr:state", "addr:postcode"} {
|
|
if values[k] != "" {
|
|
parts = append(parts, values[k])
|
|
}
|
|
}
|
|
return strings.Join(parts, ", ")
|
|
}
|
|
|
|
// Import node/way addresses from node-before-way OSM XML. Osmium's default
|
|
// tags-filter output includes the referenced nodes needed for way coordinates.
|
|
func importAddresses(input io.Reader) ([]addressPoint, error) {
|
|
decoder := xml.NewDecoder(input)
|
|
nodes := make(map[int64]addressPoint)
|
|
var points []addressPoint
|
|
for {
|
|
token, err := decoder.Token()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
start, ok := token.(xml.StartElement)
|
|
if !ok {
|
|
continue
|
|
}
|
|
switch start.Name.Local {
|
|
case "node":
|
|
var n osmNode
|
|
if err = decoder.DecodeElement(&n, &start); err != nil {
|
|
return nil, err
|
|
}
|
|
if !validCoordinate(n.Lat, n.Lon) {
|
|
continue
|
|
}
|
|
p := addressPoint{Lat: n.Lat, Lon: n.Lon, Label: addressLabel(n.Tags)}
|
|
nodes[n.ID] = p
|
|
if p.Label != "" {
|
|
points = append(points, p)
|
|
}
|
|
case "way":
|
|
var w osmWay
|
|
if err = decoder.DecodeElement(&w, &start); err != nil {
|
|
return nil, err
|
|
}
|
|
label := addressLabel(w.Tags)
|
|
if label == "" {
|
|
continue
|
|
}
|
|
refs := w.Refs
|
|
if len(refs) > 1 && refs[0].ID == refs[len(refs)-1].ID {
|
|
refs = refs[:len(refs)-1]
|
|
}
|
|
if len(refs) == 0 {
|
|
continue
|
|
}
|
|
var lat, lon float64
|
|
complete := true
|
|
for _, ref := range refs {
|
|
p, ok := nodes[ref.ID]
|
|
if !ok {
|
|
complete = false
|
|
break
|
|
}
|
|
lat += p.Lat
|
|
lon += p.Lon
|
|
}
|
|
if complete {
|
|
points = append(points, addressPoint{Lat: lat / float64(len(refs)), Lon: lon / float64(len(refs)), Label: label})
|
|
}
|
|
}
|
|
}
|
|
if len(points) == 0 {
|
|
return nil, fmt.Errorf("no usable node/way addresses found in OSM XML")
|
|
}
|
|
return points, nil
|
|
}
|
|
|
|
func buildAddresses(inputPath, outputPath string) (int, error) {
|
|
f, err := os.Open(inputPath)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer f.Close()
|
|
points, err := importAddresses(f)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if err = os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
|
|
return 0, err
|
|
}
|
|
tmp, err := os.CreateTemp(filepath.Dir(outputPath), ".addresses-*.json")
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer os.Remove(tmp.Name())
|
|
err = json.NewEncoder(tmp).Encode(points)
|
|
closeErr := tmp.Close()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if closeErr != nil {
|
|
return 0, closeErr
|
|
}
|
|
if err = os.Rename(tmp.Name(), outputPath); err != nil {
|
|
return 0, err
|
|
}
|
|
return len(points), nil
|
|
}
|