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 }