Skip to content

Commit abb7477

Browse files
committed
Support snap to road
1 parent 1a14fcf commit abb7477

2 files changed

Lines changed: 147 additions & 20 deletions

File tree

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,40 @@ Status codes:
105105
- `401` - missing or invalid API key
106106
- `429` - rate limit exceeded
107107

108+
### GET /snap
109+
110+
Snaps a coordinate to the closest point on a street.
111+
112+
Query parameters:
113+
- `lat` - latitude (required)
114+
- `lon` - longitude (required)
115+
- `key` - API key (required)
116+
- `distance` - max search distance in meters (optional, default 75)
117+
118+
Example request:
119+
120+
```
121+
GET /snap?lat=43.7384&lon=7.4246&key=YOUR_API_KEY
122+
```
123+
124+
Response:
125+
126+
```json
127+
{
128+
"lat": 43.7383,
129+
"lon": 7.4247,
130+
"distance": 4.2
131+
}
132+
```
133+
134+
`distance` is meters from the input to the snapped point.
135+
136+
Status codes:
137+
- `200` - success
138+
- `401` - missing or invalid API key
139+
- `404` - no street within `distance`
140+
- `429` - rate limit exceeded
141+
108142
### Authentication
109143

110144
The server includes a web dashboard for managing API keys. On first launch, navigate to the server URL in a browser to create an admin account. Once logged in, you can generate API keys and create additional users with configurable rate limits.

server/src/main.rs

Lines changed: 113 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,71 @@ impl Index {
509509
let display_name = format_address(&address);
510510
Address { display_name, address }
511511
}
512+
513+
fn snap(&self, lat: f64, lng: f64, search_distance: f64) -> Option<Snap> {
514+
let meters_to_rad = search_distance / 111_320.0;
515+
let max_dist = meters_to_rad * meters_to_rad;
516+
517+
let cell = cell_id_at_level(lat, lng, self.street_cell_level);
518+
let neighbors = cell_neighbors_at_level(cell, self.street_cell_level);
519+
520+
let all_ways: &[WayHeader] = unsafe {
521+
std::slice::from_raw_parts(
522+
self.street_ways.as_ptr() as *const WayHeader,
523+
self.street_ways.len() / std::mem::size_of::<WayHeader>(),
524+
)
525+
};
526+
let all_street_nodes: &[NodeCoord] = unsafe {
527+
std::slice::from_raw_parts(
528+
self.street_nodes.as_ptr() as *const NodeCoord,
529+
self.street_nodes.len() / std::mem::size_of::<NodeCoord>(),
530+
)
531+
};
532+
533+
let cos_lat = lat.to_radians().cos();
534+
535+
let mut best_dist = f64::MAX;
536+
let mut best_lat = 0.0_f64;
537+
let mut best_lng = 0.0_f64;
538+
539+
let mut seen_streets: [u32; 64] = [u32::MAX; 64];
540+
541+
for c in std::iter::once(cell).chain(neighbors.into_iter()) {
542+
let offsets = Self::lookup_geo_cell(&self.geo_cells, c);
543+
544+
Self::for_each_entry(&self.street_entries, offsets.street, |id| {
545+
let slot = (id as usize) & 0x3F;
546+
if seen_streets[slot] == id { return; }
547+
seen_streets[slot] = id;
548+
549+
let way = &all_ways[id as usize];
550+
let offset = way.node_offset as usize;
551+
let count = way.node_count as usize;
552+
let nodes = &all_street_nodes[offset..offset + count];
553+
554+
for i in 0..nodes.len() - 1 {
555+
let ax = nodes[i].lat as f64;
556+
let ay = nodes[i].lng as f64;
557+
let bx = nodes[i + 1].lat as f64;
558+
let by = nodes[i + 1].lng as f64;
559+
let (dist, t) = point_to_segment_with_t(lat, lng, ax, ay, bx, by, cos_lat);
560+
if dist < best_dist {
561+
best_dist = dist;
562+
best_lat = ax + t * (bx - ax);
563+
best_lng = ay + t * (by - ay);
564+
}
565+
}
566+
});
567+
}
568+
569+
if best_dist >= max_dist { return None; }
570+
571+
Some(Snap {
572+
lat: best_lat,
573+
lng: best_lng,
574+
distance: best_dist.sqrt() * 111_320.0,
575+
})
576+
}
512577
}
513578

514579
// --- Geometry helpers ---
@@ -609,6 +674,14 @@ struct Address<'a> {
609674
address: AddressDetails<'a>,
610675
}
611676

677+
#[derive(Serialize)]
678+
struct Snap {
679+
lat: f64,
680+
#[serde(rename = "lon")]
681+
lng: f64,
682+
distance: f64,
683+
}
684+
612685
// Address formatting patterns by country code
613686
// Returns (number_after_street, postcode_before_city, include_state)
614687
fn format_rules(country_code: Option<&str>) -> (bool, bool, bool) {
@@ -703,37 +776,56 @@ struct QueryParams {
703776
distance: Option<f64>,
704777
}
705778

779+
fn authorize(
780+
key: &Option<String>,
781+
db: &RwLock<auth::Db>,
782+
limiter: &auth::RateLimiter,
783+
addr: std::net::SocketAddr,
784+
) -> Result<(), Response> {
785+
let key = key.as_deref()
786+
.ok_or_else(|| (StatusCode::UNAUTHORIZED, "Missing API key").into_response())?;
787+
788+
let (login, rps, rpd, by_ip) = db.read().unwrap().validate_token(key)
789+
.ok_or_else(|| (StatusCode::UNAUTHORIZED, "Invalid API key").into_response())?;
790+
791+
let rate_key = if by_ip { format!("{}:{}", login, addr.ip()) } else { login };
792+
793+
auth::check_rate(limiter, &rate_key, rps, rpd)
794+
.map_err(|msg| (StatusCode::TOO_MANY_REQUESTS, msg).into_response())
795+
}
796+
797+
fn json_response<T: Serialize>(value: &T) -> Response {
798+
let json = serde_json::to_string(value).unwrap_or_default();
799+
([(axum::http::header::CONTENT_TYPE, "application/json")], json).into_response()
800+
}
801+
706802
async fn reverse_geocode(
707803
Query(params): Query<QueryParams>,
708804
state: axum::extract::State<Arc<RwLock<auth::Db>>>,
709805
index: axum::extract::Extension<Arc<Index>>,
710806
limiter: axum::extract::Extension<Arc<auth::RateLimiter>>,
711807
connect_info: axum::extract::ConnectInfo<std::net::SocketAddr>,
712808
) -> Response {
713-
let key = match params.key {
714-
Some(k) => k,
715-
None => return (StatusCode::UNAUTHORIZED, "Missing API key").into_response(),
716-
};
717-
718-
let (login, rps, rpd, by_ip) = match state.read().unwrap().validate_token(&key) {
719-
Some(info) => info,
720-
None => return (StatusCode::UNAUTHORIZED, "Invalid API key").into_response(),
721-
};
809+
if let Err(r) = authorize(&params.key, &state, &limiter, connect_info.0) { return r; }
722810

723-
let rate_key = if by_ip {
724-
format!("{}:{}", login, connect_info.0.ip())
725-
} else {
726-
login
727-
};
811+
let search_distance = params.distance.unwrap_or(DEFAULT_SEARCH_DISTANCE);
812+
json_response(&index.query(params.lat, params.lon, search_distance))
813+
}
728814

729-
if let Err(msg) = auth::check_rate(&limiter, &rate_key, rps, rpd) {
730-
return (StatusCode::TOO_MANY_REQUESTS, msg).into_response();
731-
}
815+
async fn snap_to_road(
816+
Query(params): Query<QueryParams>,
817+
state: axum::extract::State<Arc<RwLock<auth::Db>>>,
818+
index: axum::extract::Extension<Arc<Index>>,
819+
limiter: axum::extract::Extension<Arc<auth::RateLimiter>>,
820+
connect_info: axum::extract::ConnectInfo<std::net::SocketAddr>,
821+
) -> Response {
822+
if let Err(r) = authorize(&params.key, &state, &limiter, connect_info.0) { return r; }
732823

733824
let search_distance = params.distance.unwrap_or(DEFAULT_SEARCH_DISTANCE);
734-
let address = index.query(params.lat, params.lon, search_distance);
735-
let json = serde_json::to_string(&address).unwrap_or_default();
736-
([(axum::http::header::CONTENT_TYPE, "application/json")], json).into_response()
825+
match index.snap(params.lat, params.lon, search_distance) {
826+
Some(s) => json_response(&s),
827+
None => StatusCode::NOT_FOUND.into_response(),
828+
}
737829
}
738830

739831
#[tokio::main]
@@ -764,6 +856,7 @@ async fn main() {
764856

765857
let app = Router::new()
766858
.route("/reverse", get(reverse_geocode))
859+
.route("/snap", get(snap_to_road))
767860
.merge(auth::router())
768861
.layer(axum::Extension(index))
769862
.layer(axum::Extension(limiter))

0 commit comments

Comments
 (0)