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.