use crate::{
ArcGISClient, GeocodeResponse, LocationType, Result, ReverseGeocodeResponse, SuggestResponse,
};
use tracing::instrument;
pub struct GeocodeServiceClient<'a> {
base_url: String,
client: &'a ArcGISClient,
}
impl<'a> GeocodeServiceClient<'a> {
#[instrument(skip(base_url, client))]
pub fn new(base_url: impl Into<String>, client: &'a ArcGISClient) -> Self {
let base_url = base_url.into();
tracing::debug!(base_url = %base_url, "Creating GeocodeServiceClient");
Self { base_url, client }
}
#[instrument(skip(self, address), fields(base_url = %self.base_url))]
pub async fn find_address_candidates(
&self,
address: impl Into<String>,
) -> Result<GeocodeResponse> {
let address = address.into();
tracing::debug!(address = %address, "Finding address candidates");
let url = format!("{}/findAddressCandidates", self.base_url);
tracing::debug!(url = %url, "Sending findAddressCandidates request");
let mut request = self.client.http().get(&url).query(&[
("SingleLine", address.as_str()),
("f", "json"),
("outFields", "*"),
("maxLocations", "50"),
]);
if let Some(token) = self.client.get_token_if_required().await? {
request = request.query(&[("token", token)]);
}
let response = request.send().await?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|e| format!("Failed to read error: {}", e));
tracing::error!(status = %status, error = %error_text, "findAddressCandidates failed");
return Err(crate::Error::from(crate::ErrorKind::Api {
code: status.as_u16() as i32,
message: format!("HTTP {}: {}", status, error_text),
}));
}
let geocode_response: GeocodeResponse = response.json().await?;
tracing::info!(
candidate_count = geocode_response.candidates().len(),
"findAddressCandidates completed"
);
Ok(geocode_response)
}
#[instrument(skip(self, address), fields(base_url = %self.base_url))]
pub async fn find_address_candidates_with_options(
&self,
address: impl Into<String>,
max_locations: Option<u32>,
location_type: Option<LocationType>,
) -> Result<GeocodeResponse> {
let address = address.into();
tracing::debug!(address = %address, "Finding address candidates with options");
let url = format!("{}/findAddressCandidates", self.base_url);
let max_locs = max_locations.unwrap_or(50).to_string();
let mut params = vec![
("SingleLine", address.as_str()),
("f", "json"),
("outFields", "*"),
("maxLocations", max_locs.as_str()),
];
let loc_type_str;
if let Some(lt) = location_type {
loc_type_str = match lt {
LocationType::Rooftop => "rooftop",
LocationType::Street => "street",
};
params.push(("locationType", loc_type_str));
}
tracing::debug!(url = %url, "Sending findAddressCandidates request");
let mut request = self.client.http().get(&url).query(¶ms);
if let Some(token) = self.client.get_token_if_required().await? {
request = request.query(&[("token", token)]);
}
let response = request.send().await?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|e| format!("Failed to read error: {}", e));
tracing::error!(status = %status, error = %error_text, "findAddressCandidates failed");
return Err(crate::Error::from(crate::ErrorKind::Api {
code: status.as_u16() as i32,
message: format!("HTTP {}: {}", status, error_text),
}));
}
let geocode_response: GeocodeResponse = response.json().await?;
tracing::info!(
candidate_count = geocode_response.candidates().len(),
"findAddressCandidates completed"
);
Ok(geocode_response)
}
#[instrument(skip(self, address), fields(out_sr = out_sr))]
pub async fn find_address_candidates_with_sr(
&self,
address: impl Into<String>,
out_sr: i32,
) -> Result<GeocodeResponse> {
let address = address.into();
tracing::debug!(address = %address, out_sr = out_sr, "Finding address candidates with custom SR");
let url = format!("{}/findAddressCandidates", self.base_url);
tracing::debug!(url = %url, "Sending findAddressCandidates request");
let mut request = self.client.http().get(&url).query(&[
("SingleLine", address.as_str()),
("outSR", out_sr.to_string().as_str()),
("f", "json"),
("outFields", "*"),
("maxLocations", "50"),
]);
if let Some(token) = self.client.get_token_if_required().await? {
request = request.query(&[("token", token)]);
}
let response = request.send().await?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|e| format!("Failed to read error: {}", e));
tracing::error!(status = %status, error = %error_text, "findAddressCandidates with SR failed");
return Err(crate::Error::from(crate::ErrorKind::Api {
code: status.as_u16() as i32,
message: format!("HTTP {}: {}", status, error_text),
}));
}
let geocode_response: GeocodeResponse = response.json().await?;
tracing::info!(
candidate_count = geocode_response.candidates().len(),
out_sr = out_sr,
"findAddressCandidates with SR completed"
);
Ok(geocode_response)
}
#[instrument(skip(self, location), fields(
base_url = %self.base_url,
input_wkid = P::WKID,
input_sr = P::NAME
))]
pub async fn reverse_geocode<P: crate::ProjectedPoint>(
&self,
location: &P,
) -> Result<ReverseGeocodeResponse> {
self.reverse_geocode_to::<P, P>(location).await
}
#[instrument(skip(self, location), fields(
base_url = %self.base_url,
input_wkid = In::WKID,
output_wkid = Out::WKID
))]
pub async fn reverse_geocode_to<In: crate::ProjectedPoint, Out: crate::ProjectedPoint>(
&self,
location: &In,
) -> Result<ReverseGeocodeResponse> {
tracing::debug!(
input_sr = In::NAME,
output_sr = Out::NAME,
"Reverse geocoding with spatial reference conversion"
);
let url = format!("{}/reverseGeocode", self.base_url);
let location_param = location.to_location_json();
let mut request = self.client.http().get(&url).query(&[
("location", location_param.as_str()),
("outSR", Out::WKID.to_string().as_str()),
("f", "json"),
]);
if let Some(token) = self.client.get_token_if_required().await? {
request = request.query(&[("token", token)]);
}
let response = request.send().await?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|e| format!("Failed to read error: {}", e));
tracing::error!(status = %status, error = %error_text, "reverseGeocode failed");
return Err(crate::Error::from(crate::ErrorKind::Api {
code: status.as_u16() as i32,
message: format!("HTTP {}: {}", status, error_text),
}));
}
let reverse_response: ReverseGeocodeResponse = response.json().await?;
tracing::info!(
address = ?reverse_response.address().match_addr(),
input_sr = In::NAME,
output_sr = Out::NAME,
"reverseGeocode completed"
);
Ok(reverse_response)
}
#[instrument(skip(self, text), fields(base_url = %self.base_url))]
pub async fn suggest(&self, text: impl Into<String>) -> Result<SuggestResponse> {
let text = text.into();
tracing::debug!(text = %text, "Getting autocomplete suggestions");
let url = format!("{}/suggest", self.base_url);
tracing::debug!(url = %url, "Sending suggest request");
let mut request = self
.client
.http()
.get(&url)
.query(&[("text", text.as_str()), ("f", "json")]);
if let Some(token) = self.client.get_token_if_required().await? {
request = request.query(&[("token", token)]);
}
let response = request.send().await?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|e| format!("Failed to read error: {}", e));
tracing::error!(status = %status, error = %error_text, "suggest failed");
return Err(crate::Error::from(crate::ErrorKind::Api {
code: status.as_u16() as i32,
message: format!("HTTP {}: {}", status, error_text),
}));
}
let suggest_response: SuggestResponse = response.json().await?;
tracing::info!(
suggestion_count = suggest_response.suggestions().len(),
"suggest completed"
);
Ok(suggest_response)
}
#[instrument(skip(self, text, category), fields(text_len = text.as_ref().len()))]
pub async fn suggest_with_category(
&self,
text: impl Into<String> + AsRef<str>,
category: crate::Category,
) -> Result<SuggestResponse> {
let text = text.into();
tracing::debug!(text = %text, category = ?category, "Getting category-filtered suggestions");
let url = format!("{}/suggest", self.base_url);
tracing::debug!(url = %url, "Sending suggest request with category filter");
let mut request = self.client.http().get(&url).query(&[
("text", text.as_str()),
("category", category.as_str()),
("f", "json"),
]);
if let Some(token) = self.client.get_token_if_required().await? {
request = request.query(&[("token", token)]);
}
let response = request.send().await?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|e| format!("Failed to read error: {}", e));
tracing::error!(status = %status, error = %error_text, "suggest with category failed");
return Err(crate::Error::from(crate::ErrorKind::Api {
code: status.as_u16() as i32,
message: format!("HTTP {}: {}", status, error_text),
}));
}
let suggest_response: SuggestResponse = response.json().await?;
tracing::info!(
suggestion_count = suggest_response.suggestions().len(),
category = ?category,
"suggest with category completed"
);
Ok(suggest_response)
}
#[instrument(skip(self, addresses), fields(count = addresses.len()))]
pub async fn geocode_addresses(
&self,
addresses: Vec<crate::BatchGeocodeRecord>,
) -> Result<crate::BatchGeocodeResponse> {
tracing::debug!("Batch geocoding addresses");
let url = format!("{}/geocodeAddresses", self.base_url);
let records: Vec<serde_json::Value> = addresses
.iter()
.map(|addr| {
serde_json::json!({
"attributes": addr
})
})
.collect();
let addresses_json = serde_json::json!({
"records": records
});
tracing::debug!(url = %url, count = addresses.len(), "Sending geocodeAddresses request");
let addresses_str = addresses_json.to_string();
let mut form = vec![("addresses", addresses_str.as_str()), ("f", "json")];
let token_opt = self.client.get_token_if_required().await?;
let token_str;
if let Some(token) = token_opt {
token_str = token;
form.push(("token", token_str.as_str()));
}
let response = self.client.http().post(&url).form(&form).send().await?;
let status = response.status();
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|e| format!("Failed to read error: {}", e));
tracing::error!(status = %status, error = %error_text, "geocodeAddresses failed");
return Err(crate::Error::from(crate::ErrorKind::Api {
code: status.as_u16() as i32,
message: format!("HTTP {}: {}", status, error_text),
}));
}
let batch_response: crate::BatchGeocodeResponse = response.json().await?;
tracing::info!(
location_count = batch_response.locations().len(),
"geocodeAddresses completed"
);
Ok(batch_response)
}
}