Geocoding is the process of converting addresses into geographic coordinates (latitude and longitude), while reverse geocoding does the opposite - converting coordinates back into human-readable addresses. In this article, we'll build a robust geocoding system in Java using both external APIs and local databases.
Part 1: Google Maps Geocoding API Integration
Project Setup
First, add dependencies to your pom.xml:
<dependencies> <!-- HTTP Client --> <dependency> <groupId>org.apache.httpcomponents.client5</groupId> <artifactId>httpclient5</artifactId> <version>5.2.1</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.15.2</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> <!-- Logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>2.0.7</version> </dependency> </dependencies>
Google Geocoding Implementation
1. Configuration and Constants
GeocodingConfig.java
public class GeocodingConfig { private final String apiKey; private final String baseUrl; private final int maxRequestsPerSecond; public GeocodingConfig(String apiKey) { this.apiKey = apiKey; this.baseUrl = "https://maps.googleapis.com/maps/api/geocode/json"; this.maxRequestsPerSecond = 50; // Google's limit } public GeocodingConfig(String apiKey, String baseUrl, int maxRequestsPerSecond) { this.apiKey = apiKey; this.baseUrl = baseUrl; this.maxRequestsPerSecond = maxRequestsPerSecond; } // Getters public String getApiKey() { return apiKey; } public String getBaseUrl() { return baseUrl; } public int getMaxRequestsPerSecond() { return maxRequestsPerSecond; } } 2. Request/Response Models
Address.java - Represents a structured address
public class Address { private String street; private String city; private String state; private String postalCode; private String country; public Address() {} public Address(String street, String city, String state, String postalCode, String country) { this.street = street; this.city = city; this.state = state; this.postalCode = postalCode; this.country = country; } // Getters and setters public String getStreet() { return street; } public void setStreet(String street) { this.street = street; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getState() { return state; } public void setState(String state) { this.state = state; } public String getPostalCode() { return postalCode; } public void setPostalCode(String postalCode) { this.postalCode = postalCode; } public String getCountry() { return country; } public void setCountry(String country) { this.country = country; } public String toFormattedString() { return String.format("%s, %s, %s %s, %s", street, city, state, postalCode, country); } } GeocodingRequest.java - Builder for geocoding requests
import java.util.HashMap; import java.util.Map; public class GeocodingRequest { private final String address; private final Address structuredAddress; private final String components; private final String bounds; private final String region; private final String language; private GeocodingRequest(Builder builder) { this.address = builder.address; this.structuredAddress = builder.structuredAddress; this.components = builder.components; this.bounds = builder.bounds; this.region = builder.region; this.language = builder.language; } public static class Builder { private String address; private Address structuredAddress; private String components; private String bounds; private String region = "us"; private String language = "en"; public Builder address(String address) { this.address = address; return this; } public Builder structuredAddress(Address address) { this.structuredAddress = address; return this; } public Builder components(Map<String, String> components) { if (components != null && !components.isEmpty()) { StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> entry : components.entrySet()) { if (sb.length() > 0) sb.append("|"); sb.append(entry.getKey()).append(":").append(entry.getValue()); } this.components = sb.toString(); } return this; } public Builder bounds(double south, double west, double north, double east) { this.bounds = String.format("%f,%f|%f,%f", south, west, north, east); return this; } public Builder region(String region) { this.region = region; return this; } public Builder language(String language) { this.language = language; return this; } public GeocodingRequest build() { if (address == null && structuredAddress == null) { throw new IllegalStateException("Either address or structuredAddress must be provided"); } return new GeocodingRequest(this); } } // Getters public String getAddress() { return address; } public Address getStructuredAddress() { return structuredAddress; } public String getComponents() { return components; } public String getBounds() { return bounds; } public String getRegion() { return region; } public String getLanguage() { return language; } public String toQueryString() { StringBuilder sb = new StringBuilder(); if (address != null) { sb.append("address=").append(java.net.URLEncoder.encode(address, java.nio.charset.StandardCharsets.UTF_8)); } else if (structuredAddress != null) { sb.append("address=").append(java.net.URLEncoder.encode(structuredAddress.toFormattedString(), java.nio.charset.StandardCharsets.UTF_8)); } if (components != null) { sb.append("&components=").append(java.net.URLEncoder.encode(components, java.nio.charset.StandardCharsets.UTF_8)); } if (bounds != null) { sb.append("&bounds=").append(bounds); } if (region != null) { sb.append("®ion=").append(region); } if (language != null) { sb.append("&language=").append(language); } return sb.toString(); } } 3. Response Models
GeocodingResponse.java
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; @JsonIgnoreProperties(ignoreUnknown = true) public class GeocodingResponse { private List<Result> results; private String status; @JsonProperty("error_message") private String errorMessage; // Getters and setters public List<Result> getResults() { return results; } public void setResults(List<Result> results) { this.results = results; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public String getErrorMessage() { return errorMessage; } public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } } Result.java
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; @JsonIgnoreProperties(ignoreUnknown = true) public class Result { @JsonProperty("address_components") private List<AddressComponent> addressComponents; @JsonProperty("formatted_address") private String formattedAddress; private Geometry geometry; @JsonProperty("place_id") private String placeId; private List<String> types; @JsonProperty("partial_match") private boolean partialMatch; @JsonProperty("postcode_localities") private List<String> postcodeLocalities; // Getters and setters public List<AddressComponent> getAddressComponents() { return addressComponents; } public void setAddressComponents(List<AddressComponent> addressComponents) { this.addressComponents = addressComponents; } public String getFormattedAddress() { return formattedAddress; } public void setFormattedAddress(String formattedAddress) { this.formattedAddress = formattedAddress; } public Geometry getGeometry() { return geometry; } public void setGeometry(Geometry geometry) { this.geometry = geometry; } public String getPlaceId() { return placeId; } public void setPlaceId(String placeId) { this.placeId = placeId; } public List<String> getTypes() { return types; } public void setTypes(List<String> types) { this.types = types; } public boolean isPartialMatch() { return partialMatch; } public void setPartialMatch(boolean partialMatch) { this.partialMatch = partialMatch; } public List<String> getPostcodeLocalities() { return postcodeLocalities; } public void setPostcodeLocalities(List<String> postcodeLocalities) { this.postcodeLocalities = postcodeLocalities; } } AddressComponent.java
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; @JsonIgnoreProperties(ignoreUnknown = true) public class AddressComponent { @JsonProperty("long_name") private String longName; @JsonProperty("short_name") private String shortName; private List<String> types; // Getters and setters public String getLongName() { return longName; } public void setLongName(String longName) { this.longName = longName; } public String getShortName() { return shortName; } public void setShortName(String shortName) { this.shortName = shortName; } public List<String> getTypes() { return types; } public void setTypes(List<String> types) { this.types = types; } } Geometry.java
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @JsonIgnoreProperties(ignoreUnknown = true) public class Geometry { private Location location; @JsonProperty("location_type") private String locationType; private Bounds bounds; private Bounds viewport; // Getters and setters public Location getLocation() { return location; } public void setLocation(Location location) { this.location = location; } public String getLocationType() { return locationType; } public void setLocationType(String locationType) { this.locationType = locationType; } public Bounds getBounds() { return bounds; } public void setBounds(Bounds bounds) { this.bounds = bounds; } public Bounds getViewport() { return viewport; } public void setViewport(Bounds viewport) { this.viewport = viewport; } } Location.java
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @JsonIgnoreProperties(ignoreUnknown = true) public class Location { private double lat; private double lng; public Location() {} public Location(double lat, double lng) { this.lat = lat; this.lng = lng; } // Getters and setters public double getLat() { return lat; } public void setLat(double lat) { this.lat = lat; } public double getLng() { return lng; } public void setLng(double lng) { this.lng = lng; } @Override public String toString() { return String.format("(%.6f, %.6f)", lat, lng); } } Bounds.java
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @JsonIgnoreProperties(ignoreUnknown = true) public class Bounds { private Location northeast; private Location southwest; // Getters and setters public Location getNortheast() { return northeast; } public void setNortheast(Location northeast) { this.northeast = northeast; } public Location getSouthwest() { return southwest; } public void setSouthwest(Location southwest) { this.southwest = southwest; } } 4. Core Geocoding Service
GoogleGeocodingService.java
import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.concurrent.TimeUnit; public class GoogleGeocodingService { private static final Logger logger = LoggerFactory.getLogger(GoogleGeocodingService.class); private final GeocodingConfig config; private final CloseableHttpClient httpClient; private final ObjectMapper objectMapper; private long lastRequestTime = 0; public GoogleGeocodingService(GeocodingConfig config) { this.config = config; this.httpClient = HttpClients.createDefault(); this.objectMapper = new ObjectMapper(); } public GeocodingResponse geocode(GeocodingRequest request) { rateLimit(); String url = buildRequestUrl(request); logger.info("Geocoding request: {}", url); HttpGet httpGet = new HttpGet(url); try (CloseableHttpResponse response = httpClient.execute(httpGet)) { String responseBody = EntityUtils.toString(response.getEntity()); if (response.getCode() != 200) { throw new RuntimeException("HTTP error: " + response.getCode() + " - " + responseBody); } GeocodingResponse geocodingResponse = objectMapper.readValue(responseBody, GeocodingResponse.class); // Check API status if (!"OK".equals(geocodingResponse.getStatus()) && !"ZERO_RESULTS".equals(geocodingResponse.getStatus())) { throw new RuntimeException("Geocoding API Error: " + geocodingResponse.getStatus() + " - " + geocodingResponse.getErrorMessage()); } return geocodingResponse; } catch (IOException e) { throw new RuntimeException("Failed to execute geocoding request", e); } } public Location geocodeToLocation(String address) { GeocodingRequest request = new GeocodingRequest.Builder() .address(address) .build(); GeocodingResponse response = geocode(request); if (response.getResults() != null && !response.getResults().isEmpty()) { return response.getResults().get(0).getGeometry().getLocation(); } throw new RuntimeException("No results found for address: " + address); } public GeocodingResponse reverseGeocode(double lat, double lng) { rateLimit(); String url = String.format("%s?latlng=%.6f,%.6f&key=%s", config.getBaseUrl(), lat, lng, config.getApiKey()); HttpGet httpGet = new HttpGet(url); try (CloseableHttpResponse response = httpClient.execute(httpGet)) { String responseBody = EntityUtils.toString(response.getEntity()); return objectMapper.readValue(responseBody, GeocodingResponse.class); } catch (IOException e) { throw new RuntimeException("Failed to execute reverse geocoding request", e); } } private String buildRequestUrl(GeocodingRequest request) { return config.getBaseUrl() + "?" + request.toQueryString() + "&key=" + config.getApiKey(); } private void rateLimit() { long currentTime = System.currentTimeMillis(); long timeSinceLastRequest = currentTime - lastRequestTime; long minInterval = 1000 / config.getMaxRequestsPerSecond(); if (timeSinceLastRequest < minInterval) { try { TimeUnit.MILLISECONDS.sleep(minInterval - timeSinceLastRequest); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } lastRequestTime = System.currentTimeMillis(); } public void close() throws IOException { httpClient.close(); } } Part 2: Local Geocoding with OpenStreetMap (Nomination)
For applications that need free, unlimited geocoding without API keys.
5. Local Geocoding Service
LocalGeocodingService.java
import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.core5.http.io.entity.EntityUtils; import java.io.IOException; import java.util.List; public class LocalGeocodingService { private final CloseableHttpClient httpClient; private final ObjectMapper objectMapper; private final String baseUrl; public LocalGeocodingService() { this("https://nominatim.openstreetmap.org"); } public LocalGeocodingService(String baseUrl) { this.httpClient = HttpClients.createDefault(); this.objectMapper = new ObjectMapper(); this.baseUrl = baseUrl; } public List<NominatimResult> search(String query, int limit) { String url = String.format("%s/search?format=json&q=%s&limit=%d", baseUrl, java.net.URLEncoder.encode(query, java.nio.charset.StandardCharsets.UTF_8), limit); HttpGet httpGet = new HttpGet(url); httpGet.setHeader("User-Agent", "JavaGeocodingClient/1.0"); try (CloseableHttpResponse response = httpClient.execute(httpGet)) { String responseBody = EntityUtils.toString(response.getEntity()); return objectMapper.readValue(responseBody, objectMapper.getTypeFactory().constructCollectionType(List.class, NominatimResult.class)); } catch (IOException e) { throw new RuntimeException("Failed to execute local geocoding request", e); } } public List<NominatimResult> reverse(double lat, double lng) { String url = String.format("%s/reverse?format=json&lat=%.6f&lon=%.6f", baseUrl, lat, lng); HttpGet httpGet = new HttpGet(url); httpGet.setHeader("User-Agent", "JavaGeocodingClient/1.0"); try (CloseableHttpResponse response = httpClient.execute(httpGet)) { String responseBody = EntityUtils.toString(response.getEntity()); NominatimResult result = objectMapper.readValue(responseBody, NominatimResult.class); return List.of(result); } catch (IOException e) { throw new RuntimeException("Failed to execute local reverse geocoding request", e); } } public void close() throws IOException { httpClient.close(); } } NominatimResult.java
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @JsonIgnoreProperties(ignoreUnknown = true) public class NominatimResult { @JsonProperty("place_id") private long placeId; private String licence; @JsonProperty("osm_type") private String osmType; @JsonProperty("osm_id") private long osmId; private boundingbox boundingbox; private String lat; private String lon; @JsonProperty("display_name") private String displayName; private String category; private String type; private double importance; // Getters and setters public long getPlaceId() { return placeId; } public void setPlaceId(long placeId) { this.placeId = placeId; } public String getLicence() { return licence; } public void setLicence(String licence) { this.licence = licence; } public String getOsmType() { return osmType; } public void setOsmType(String osmType) { this.osmType = osmType; } public long getOsmId() { return osmId; } public void setOsmId(long osmId) { this.osmId = osmId; } public String getLat() { return lat; } public void setLat(String lat) { this.lat = lat; } public String getLon() { return lon; } public void setLon(String lon) { this.lon = lon; } public String getDisplayName() { return displayName; } public void setDisplayName(String displayName) { this.displayName = displayName; } public String getCategory() { return category; } public void setCategory(String category) { this.category = category; } public String getType() { return type; } public void setType(String type) { this.type = type; } public double getImportance() { return importance; } public void setImportance(double importance) { this.importance = importance; } public Location toLocation() { return new Location(Double.parseDouble(lat), Double.parseDouble(lon)); } } 6. Demonstration Class
GeocodingDemo.java
import java.util.HashMap; import java.util.Map; public class GeocodingDemo { public static void main(String[] args) { // Example 1: Google Geocoding API googleGeocodingExample(); // Example 2: Local Geocoding with OpenStreetMap localGeocodingExample(); // Example 3: Reverse Geocoding reverseGeocodingExample(); } private static void googleGeocodingExample() { System.out.println("=== Google Geocoding API Example ==="); String apiKey = "YOUR_GOOGLE_API_KEY_HERE"; // Replace with your actual API key GeocodingConfig config = new GeocodingConfig(apiKey); try (GoogleGeocodingService service = new GoogleGeocodingService(config)) { // Example 1: Simple address geocoding GeocodingRequest request1 = new GeocodingRequest.Builder() .address("1600 Amphitheatre Parkway, Mountain View, CA") .build(); GeocodingResponse response1 = service.geocode(request1); printGoogleResults(response1); // Example 2: Structured address Address address = new Address( "350 5th Ave", "New York", "NY", "10118", "USA" ); GeocodingRequest request2 = new GeocodingRequest.Builder() .structuredAddress(address) .build(); GeocodingResponse response2 = service.geocode(request2); printGoogleResults(response2); // Example 3: With components and bounds Map<String, String> components = new HashMap<>(); components.put("country", "US"); components.put("postal_code", "94105"); GeocodingRequest request3 = new GeocodingRequest.Builder() .address("Market Street") .components(components) .bounds(37.7, -122.5, 37.8, -122.4) // San Francisco bounds .build(); GeocodingResponse response3 = service.geocode(request3); printGoogleResults(response3); } catch (Exception e) { System.err.println("Error in Google geocoding: " + e.getMessage()); } } private static void localGeocodingExample() { System.out.println("\n=== Local Geocoding (OpenStreetMap) Example ==="); try (LocalGeocodingService service = new LocalGeocodingService()) { // Search for addresses var results = service.search("Eiffel Tower, Paris", 3); for (int i = 0; i < results.size(); i++) { NominatimResult result = results.get(i); System.out.printf("Result %d:%n", i + 1); System.out.printf(" Display Name: %s%n", result.getDisplayName()); System.out.printf(" Location: %s%n", result.toLocation()); System.out.printf(" Type: %s, Importance: %.2f%n", result.getType(), result.getImportance()); System.out.println(); } } catch (Exception e) { System.err.println("Error in local geocoding: " + e.getMessage()); } } private static void reverseGeocodingExample() { System.out.println("=== Reverse Geocoding Example ==="); String apiKey = "YOUR_GOOGLE_API_KEY_HERE"; GeocodingConfig config = new GeocodingConfig(apiKey); try (GoogleGeocodingService service = new GoogleGeocodingService(config)) { // Reverse geocode coordinates double lat = 40.748817; double lng = -73.985428; GeocodingResponse response = service.reverseGeocode(lat, lng); System.out.printf("Reverse geocoding for coordinates (%.6f, %.6f):%n", lat, lng); if (response.getResults() != null && !response.getResults().isEmpty()) { System.out.printf("Address: %s%n", response.getResults().get(0).getFormattedAddress()); } } catch (Exception e) { System.err.println("Error in reverse geocoding: " + e.getMessage()); } } private static void printGoogleResults(GeocodingResponse response) { System.out.printf("Status: %s%n", response.getStatus()); if (response.getResults() != null) { for (int i = 0; i < response.getResults().size(); i++) { Result result = response.getResults().get(i); System.out.printf("Result %d:%n", i + 1); System.out.printf(" Formatted Address: %s%n", result.getFormattedAddress()); System.out.printf(" Location: %s%n", result.getGeometry().getLocation()); System.out.printf(" Location Type: %s%n", result.getGeometry().getLocationType()); System.out.printf(" Types: %s%n", String.join(", ", result.getTypes())); System.out.println(); } } System.out.println("---"); } } Expected Output
=== Google Geocoding API Example === Status: OK Result 1: Formatted Address: 1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA Location: (37.422408, -122.085609) Location Type: ROOFTOP Types: street_address
Best Practices
- Caching: Implement caching for frequently geocoded addresses to reduce API calls
- Error Handling: Handle different status codes (OVER_QUERY_LIMIT, REQUEST_DENIED, etc.)
- Rate Limiting: Respect API rate limits to avoid being blocked
- Input Validation: Validate and normalize addresses before geocoding
- Fallback Strategy: Use multiple geocoding services as fallbacks
- Batch Processing: For large datasets, implement batch geocoding with proper throttling
This implementation provides a complete geocoding solution that can handle both simple and complex geocoding scenarios while being extensible for different geocoding providers.
Java Observability, Logging Intelligence & AI-Driven Monitoring (APM, Tracing, Logs & Anomaly Detection)
https://macronepal.com/blog/beyond-metrics-observing-serverless-and-traditional-java-applications-with-thundra-apm/
Explains using Thundra APM to observe both serverless and traditional Java applications by combining tracing, metrics, and logs into a unified observability platform for faster debugging and performance insights.
https://macronepal.com/blog/dynatrace-oneagent-in-java-2/
Explains Dynatrace OneAgent for Java, which automatically instruments JVM applications to capture metrics, traces, and logs, enabling full-stack monitoring and root-cause analysis with minimal configuration.
https://macronepal.com/blog/lightstep-java-sdk-distributed-tracing-and-observability-implementation/
Explains Lightstep Java SDK for distributed tracing, helping developers track requests across microservices and identify latency issues using OpenTelemetry-based observability.
https://macronepal.com/blog/honeycomb-io-beeline-for-java-complete-guide-2/
Explains Honeycomb Beeline for Java, which provides high-cardinality observability and deep query capabilities to understand complex system behavior and debug distributed systems efficiently.
https://macronepal.com/blog/lumigo-for-serverless-in-java-complete-distributed-tracing-guide-2/
Explains Lumigo for Java serverless applications, offering automatic distributed tracing, log correlation, and error tracking to simplify debugging in cloud-native environments. (Lumigo Docs)
https://macronepal.com/blog/from-noise-to-signals-implementing-log-anomaly-detection-in-java-applications/
Explains how to detect anomalies in Java logs using behavioral patterns and machine learning techniques to separate meaningful incidents from noisy log data and improve incident response.
https://macronepal.com/blog/ai-powered-log-analysis-in-java-from-reactive-debugging-to-proactive-insights/
Explains AI-driven log analysis for Java applications, shifting from manual debugging to predictive insights that identify issues early and improve system reliability using intelligent log processing.
https://macronepal.com/blog/titliel-java-logging-best-practices/
Explains best practices for Java logging, focusing on structured logs, proper log levels, performance optimization, and ensuring logs are useful for debugging and observability systems.
https://macronepal.com/blog/seeking-a-loguru-for-java-the-quest-for-elegant-and-simple-logging/
Explains the search for simpler, more elegant logging frameworks in Java, comparing modern logging approaches that aim to reduce complexity while improving readability and developer experience.