use anyhow::Result;
use arcgis::example_tracker::ExampleTracker;
use arcgis::{
ApiKeyAuth, ApiKeyTier, ArcGISClient, BatchGeocodeRecord, Category, GeocodeServiceClient,
LocationType, WebMercatorPoint, Wgs84Point,
};
const WORLD_GEOCODE_SERVICE: &str =
"https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer";
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();
let tracker = ExampleTracker::new("geocoding_batch_operations")
.service_type("ExampleClient")
.start();
tracing::info!("🌍 Batch Geocoding Operations Examples");
tracing::info!("Demonstrating efficient bulk address processing");
tracing::info!("");
tracing::debug!("Creating geocoding service client");
let auth = ApiKeyAuth::from_env(ApiKeyTier::Location)?;
let client = ArcGISClient::new(auth);
let geocoder = GeocodeServiceClient::new(WORLD_GEOCODE_SERVICE, &client);
demonstrate_batch_geocode(&geocoder).await?;
demonstrate_advanced_options(&geocoder).await?;
demonstrate_custom_spatial_reference(&geocoder).await?;
demonstrate_reverse_geocode_custom_sr(&geocoder).await?;
demonstrate_suggest_with_category(&geocoder).await?;
tracing::info!("\n✅ All batch geocoding examples completed successfully!");
print_best_practices();
tracker.success();
Ok(())
}
async fn demonstrate_batch_geocode(geocoder: &GeocodeServiceClient<'_>) -> Result<()> {
tracing::info!("\n=== Example 1: Batch Geocoding ===");
tracing::info!("Process multiple addresses in a single API request");
tracing::info!("");
let addresses = vec![
BatchGeocodeRecord::with_single_line(1, "380 New York St, Redlands, CA 92373"),
BatchGeocodeRecord::with_single_line(2, "1 Microsoft Way, Redmond, WA"),
BatchGeocodeRecord::with_single_line(3, "1600 Amphitheatre Parkway, Mountain View, CA"),
BatchGeocodeRecord::with_single_line(4, "1 Infinite Loop, Cupertino, CA"),
];
tracing::info!(
address_count = addresses.len(),
"Geocoding {} addresses in batch",
addresses.len()
);
let response = geocoder.geocode_addresses(addresses).await?;
anyhow::ensure!(
!response.locations().is_empty(),
"Batch geocode should return results. Got 0 locations."
);
anyhow::ensure!(
response.locations().len() == 4,
"Expected 4 geocoded locations, got {}",
response.locations().len()
);
tracing::info!(
"✅ Successfully geocoded {} addresses",
response.locations().len()
);
tracing::info!("");
for (idx, location) in response.locations().iter().enumerate() {
tracing::info!(
" {}. {} → ({:.4}, {:.4}) [score: {:.1}]",
idx + 1,
location.address(),
*location.location().x(),
*location.location().y(),
*location.score()
);
anyhow::ensure!(
!location.address().is_empty(),
"Location {} should have an address",
idx
);
anyhow::ensure!(
*location.score() >= 0.0 && *location.score() <= 100.0,
"Score should be 0-100, got {}",
location.score()
);
anyhow::ensure!(
*location.location().x() >= -180.0 && *location.location().x() <= 180.0,
"Longitude should be -180 to 180, got {}",
location.location().x()
);
anyhow::ensure!(
*location.location().y() >= -90.0 && *location.location().y() <= 90.0,
"Latitude should be -90 to 90, got {}",
location.location().y()
);
}
tracing::info!("");
tracing::info!("💡 Batch geocoding benefits:");
tracing::info!(" • Single API request for multiple addresses");
tracing::info!(" • Reduced network overhead");
tracing::info!(" • More efficient credit usage");
tracing::info!(" • Ideal for processing CSV/Excel files");
Ok(())
}
async fn demonstrate_advanced_options(geocoder: &GeocodeServiceClient<'_>) -> Result<()> {
tracing::info!("\n=== Example 2: Advanced Geocoding Options ===");
tracing::info!("Use max_locations and location_type filters for precise control");
tracing::info!("");
let test_address = "Main St";
tracing::info!("Testing max_locations parameter:");
let response_limited = geocoder
.find_address_candidates_with_options(test_address, Some(3), None)
.await?;
anyhow::ensure!(
!response_limited.candidates().is_empty(),
"Should find candidates for '{}'",
test_address
);
anyhow::ensure!(
response_limited.candidates().len() <= 3,
"max_locations=3 should return ≤3 results, got {}",
response_limited.candidates().len()
);
tracing::info!(
" ✅ Requested max 3 locations, got {} candidates",
response_limited.candidates().len()
);
tracing::info!("");
tracing::info!("Testing location_type parameter:");
let precise_address = "380 New York St, Redlands, CA";
let response_rooftop = geocoder
.find_address_candidates_with_options(precise_address, Some(5), Some(LocationType::Rooftop))
.await?;
anyhow::ensure!(
!response_rooftop.candidates().is_empty(),
"Should find rooftop candidates for precise address"
);
tracing::info!(
" ✅ Found {} rooftop-level candidates",
response_rooftop.candidates().len()
);
tracing::info!("");
tracing::info!(" Top candidates for '{}':", precise_address);
for (idx, candidate) in response_rooftop.candidates().iter().take(3).enumerate() {
tracing::info!(
" {}. {} [score: {:.1}]",
idx + 1,
candidate.address(),
*candidate.score()
);
anyhow::ensure!(
!candidate.address().is_empty(),
"Candidate {} should have an address",
idx
);
anyhow::ensure!(
*candidate.score() > 0.0,
"Candidate {} should have positive score",
idx
);
}
tracing::info!("");
tracing::info!("💡 Advanced options:");
tracing::info!(" • max_locations: Control result count (default: varies)");
tracing::info!(" • location_type:");
tracing::info!(" - Rooftop: Precise building-level coordinates");
tracing::info!(" - Street: Street centerline coordinates");
tracing::info!(" • Combine both for fine-grained control");
Ok(())
}
async fn demonstrate_custom_spatial_reference(geocoder: &GeocodeServiceClient<'_>) -> Result<()> {
tracing::info!("\n=== Example 3: Custom Spatial Reference ===");
tracing::info!("Geocode with Web Mercator projection (EPSG:3857)");
tracing::info!("");
let test_address = "380 New York St, Redlands, CA 92373";
tracing::info!("Geocoding with SR 3857 (Web Mercator):");
let response = geocoder
.find_address_candidates_with_sr(test_address, 3857)
.await?;
anyhow::ensure!(
!response.candidates().is_empty(),
"Should find candidates for known address"
);
let candidate = &response.candidates()[0];
anyhow::ensure!(
candidate.location().x().abs() > 1_000_000.0,
"Web Mercator X should be large (>1M), got {}",
candidate.location().x()
);
anyhow::ensure!(
candidate.location().y().abs() > 1_000_000.0,
"Web Mercator Y should be large (>1M), got {}",
candidate.location().y()
);
tracing::info!(" ✅ Address: {}", candidate.address());
tracing::info!(
" ✅ Web Mercator coordinates: ({:.2}, {:.2})",
candidate.location().x(),
candidate.location().y()
);
tracing::info!(" ✅ Score: {:.1}", candidate.score());
tracing::info!("");
tracing::info!("💡 Custom spatial reference use cases:");
tracing::info!(" • Match your application's projection system");
tracing::info!(" • Avoid client-side reprojection");
tracing::info!(" • Web Mercator (3857) for web mapping");
tracing::info!(" • State Plane for regional accuracy");
Ok(())
}
async fn demonstrate_reverse_geocode_custom_sr(geocoder: &GeocodeServiceClient<'_>) -> Result<()> {
tracing::info!("\n=== Example 4: Type-Safe Reverse Geocoding ===");
tracing::info!("Using ProjectedPoint types for compile-time spatial reference safety");
tracing::info!("");
let wgs84 = Wgs84Point::new(-117.195, 34.056);
tracing::info!("Reverse geocoding with WGS84 → Web Mercator conversion:");
tracing::info!(
" Input: ({:.6}, {:.6}) [WGS84/EPSG:4326]",
wgs84.lon(),
wgs84.lat()
);
tracing::info!(" Output: Web Mercator (EPSG:3857)");
tracing::info!("");
let response = geocoder
.reverse_geocode_to::<_, WebMercatorPoint>(&wgs84)
.await?;
let address_str = response
.address()
.match_addr()
.as_ref()
.map(|s| s.as_str())
.unwrap_or("(no address)");
anyhow::ensure!(
!address_str.is_empty(),
"Should return an address for valid coordinates"
);
anyhow::ensure!(
response.location().x().abs() > 1_000_000.0,
"Returned location should be in Web Mercator (large X)"
);
tracing::info!(" ✅ Address: {}", address_str);
tracing::info!(
" ✅ Web Mercator location: ({:.2}, {:.2})",
response.location().x(),
response.location().y()
);
tracing::info!("");
tracing::info!("💡 Type-Safe Spatial References:");
tracing::info!(" • Wgs84Point: EPSG:4326 (GPS coordinates)");
tracing::info!(" • WebMercatorPoint: EPSG:3857 (web maps)");
tracing::info!(" • StatePlanePoint<WKID>: State Plane zones");
tracing::info!(" • Spatial reference is encoded in the type system");
tracing::info!(" • Compiler prevents mixing coordinate systems");
tracing::info!(" • No magic WKID numbers - types carry the information!");
Ok(())
}
async fn demonstrate_suggest_with_category(geocoder: &GeocodeServiceClient<'_>) -> Result<()> {
tracing::info!("\n=== Example 5: Category-Filtered Suggestions ===");
tracing::info!("Autocomplete with POI category filters");
tracing::info!("");
let query = "starbucks";
let category = Category::Food;
tracing::info!("Searching for '{}' in category: {:?}", query, category);
let response = geocoder.suggest_with_category(query, category).await?;
anyhow::ensure!(
!response.suggestions().is_empty(),
"Should find suggestions for '{}' with category filter",
query
);
let suggestion_count = response.suggestions().len();
anyhow::ensure!(suggestion_count > 0, "Expected suggestions, got 0");
tracing::info!(" ✅ Found {} suggestions", suggestion_count);
tracing::info!("");
tracing::info!(" Top suggestions:");
for (idx, suggestion) in response.suggestions().iter().take(5).enumerate() {
tracing::info!(" {}. {}", idx + 1, suggestion.text());
anyhow::ensure!(
!suggestion.text().is_empty(),
"Suggestion {} should have text",
idx
);
}
tracing::info!("");
tracing::info!("💡 Category-filtered suggestions:");
tracing::info!(" • Narrow autocomplete to specific POI types");
tracing::info!(" • Available categories: Restaurant, Hotel, Airport, etc.");
tracing::info!(" • Improves relevance for type-ahead search");
tracing::info!(" • Reduces noise in suggestion results");
Ok(())
}
fn print_best_practices() {
tracing::info!("\n💡 Batch Geocoding Best Practices:");
tracing::info!(" - Use geocode_addresses() for bulk geocoding");
tracing::info!(" - Use find_address_candidates() in a loop for multiple match options");
tracing::info!(" - Batch operations are more efficient than individual requests");
tracing::info!(" - Process in chunks of 100-1000 addresses per request");
tracing::info!(" - Always validate scores before accepting results");
tracing::info!("");
tracing::info!("📊 Credit Usage:");
tracing::info!(" - geocode_addresses: ~0.004 credits per address");
tracing::info!(" - find_address_candidates: ~0.004 credits per address");
tracing::info!(" - Batch operations have no additional overhead");
tracing::info!(" - Cache results to avoid re-geocoding");
tracing::info!("");
tracing::info!("⚡ Performance Optimization:");
tracing::info!(" - Batch size: 100-1000 addresses optimal");
tracing::info!(" - Parallel batches: Run multiple batches concurrently");
tracing::info!(" - Pre-filter: Remove duplicates before geocoding");
tracing::info!(" - Retry strategy: Implement exponential backoff for failures");
tracing::info!("");
tracing::info!("🎯 Quality Control:");
tracing::info!(" - Accept scores ≥90 automatically");
tracing::info!(" - Flag scores 70-89 for manual review");
tracing::info!(" - Reject scores <70");
tracing::info!(" - Use find_address_candidates for ambiguous addresses");
tracing::info!("");
tracing::info!("⚙️ Error Handling:");
tracing::info!(" - Check each result individually (some may fail)");
tracing::info!(" - Log failed addresses for manual processing");
tracing::info!(" - Implement retry logic for network failures");
tracing::info!(" - Monitor rate limits and implement backoff");
}