OpenStreetMap Nominatim is a powerful open-source geocoding service that converts addresses into geographic coordinates (forward geocoding) and coordinates into addresses (reverse geocoding). This guide demonstrates how to integrate Nominatim into Java applications for comprehensive location-based services.
Why Use OpenStreetMap Nominatim?
- Free and Open Source: No usage costs or API keys required
- Global Coverage: Worldwide address and place data
- Multiple Operations: Forward/reverse geocoding, place search, address lookup
- Open Data: Based on OpenStreetMap's collaborative mapping data
- Flexible Usage: Suitable for both small and large-scale applications
Prerequisites
- Java 8+ with HTTP client capabilities
- Maven/Gradle for dependency management
- Internet connection for API access
Step 1: Project Dependencies
Maven (pom.xml):
<dependencies> <!-- HTTP Client --> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.14</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> </artifactId> <!-- Caching --> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>3.1.6</version> </dependency> <!-- Logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.7</version> </dependency> <!-- Spring Boot (Optional) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.7.0</version> </dependency> <!-- Validation --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> <version>2.7.0</version> </dependency> </dependencies>
Step 2: Configuration Class
@Configuration
@ConfigurationProperties(prefix = "nominatim")
@Data
public class NominatimConfig {
private String baseUrl = "https://nominatim.openstreetmap.org";
private String userAgent = "YourApp/1.0";
private String email; // Optional: for usage policy compliance
private int timeout = 30000;
private int cacheSize = 1000;
private long cacheExpiryHours = 24;
private boolean rateLimitEnabled = true;
private int requestsPerSecond = 1; // Nominatim's usage policy
public String getSearchUrl() {
return baseUrl + "/search";
}
public String getReverseUrl() {
return baseUrl + "/reverse";
}
public String getLookupUrl() {
return baseUrl + "/lookup";
}
}
@Component
@Slf4j
public class NominatimHttpClient {
private final NominatimConfig config;
private final CloseableHttpClient httpClient;
private final ObjectMapper objectMapper;
private final RateLimiter rateLimiter;
public NominatimHttpClient(NominatimConfig config) {
this.config = config;
this.objectMapper = new ObjectMapper();
// Configure HTTP client with custom user agent
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(config.getTimeout())
.setSocketTimeout(config.getTimeout())
.build();
this.httpClient = HttpClients.custom()
.setDefaultRequestConfig(requestConfig)
.setUserAgent(config.getUserAgent())
.build();
// Initialize rate limiter
this.rateLimiter = RateLimiter.create(config.getRequestsPerSecond());
}
public <T> T executeRequest(String url, Map<String, String> params, Class<T> responseType)
throws NominatimException {
try {
// Apply rate limiting if enabled
if (config.isRateLimitEnabled()) {
rateLimiter.acquire();
}
String fullUrl = buildUrlWithParams(url, params);
HttpGet request = new HttpGet(fullUrl);
log.debug("Executing Nominatim request: {}", fullUrl);
try (CloseableHttpResponse response = httpClient.execute(request)) {
String responseBody = EntityUtils.toString(response.getEntity());
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != 200) {
throw new NominatimException("HTTP error: " + statusCode + " - " + responseBody);
}
return objectMapper.readValue(responseBody, responseType);
}
} catch (Exception e) {
throw new NominatimException("Error executing Nominatim request", e);
}
}
private String buildUrlWithParams(String url, Map<String, String> params) {
StringBuilder urlBuilder = new StringBuilder(url);
urlBuilder.append("?");
urlBuilder.append("format=json"); // Always request JSON format
urlBuilder.append("&addressdetails=1"); // Include detailed address components
// Add email if configured
if (config.getEmail() != null && !config.getEmail().isEmpty()) {
urlBuilder.append("&email=").append(URLEncoder.encode(config.getEmail(), StandardCharsets.UTF_8));
}
// Add other parameters
if (params != null) {
for (Map.Entry<String, String> entry : params.entrySet()) {
urlBuilder.append("&")
.append(entry.getKey())
.append("=")
.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8));
}
}
return urlBuilder.toString();
}
}
Step 3: Core Service Classes
Geocoding Service
@Service
@Slf4j
public class NominatimGeocodingService {
private final NominatimHttpClient nominatimClient;
private final NominatimConfig config;
public NominatimGeocodingService(NominatimHttpClient nominatimClient, NominatimConfig config) {
this.nominatimClient = nominatimClient;
this.config = config;
}
@Cacheable(value = "forwardGeocoding", key = "#query + '|' + #countryCode + '|' + #limit")
public List<NominatimResult> forwardGeocode(ForwardGeocodeRequest request) throws NominatimException {
try {
Map<String, String> params = new HashMap<>();
params.put("q", request.getQuery());
if (request.getCountryCode() != null) {
params.put("countrycodes", request.getCountryCode());
}
if (request.getLimit() > 0) {
params.put("limit", String.valueOf(request.getLimit()));
}
if (request.getViewbox() != null) {
params.put("viewbox", request.getViewbox());
}
if (request.isBounded()) {
params.put("bounded", "1");
}
params.put("accept-language", request.getLanguage());
NominatimResult[] results = nominatimClient.executeRequest(
config.getSearchUrl(), params, NominatimResult[].class);
return Arrays.asList(results);
} catch (Exception e) {
throw new NominatimException("Error in forward geocoding for query: " + request.getQuery(), e);
}
}
@Cacheable(value = "reverseGeocoding", key = "#latitude + '|' + #longitude + '|' + #zoom")
public NominatimResult reverseGeocode(double latitude, double longitude, int zoom)
throws NominatimException {
try {
Map<String, String> params = new HashMap<>();
params.put("lat", String.valueOf(latitude));
params.put("lon", String.valueOf(longitude));
params.put("zoom", String.valueOf(zoom));
params.put("accept-language", "en"); // Default language
return nominatimClient.executeRequest(
config.getReverseUrl(), params, NominatimResult.class);
} catch (Exception e) {
throw new NominatimException(
"Error in reverse geocoding for coordinates: " + latitude + "," + longitude, e);
}
}
public List<NominatimResult> searchByAddress(AddressComponents address) throws NominatimException {
try {
Map<String, String> params = new HashMap<>();
if (address.getStreet() != null) {
params.put("street", address.getStreet());
}
if (address.getCity() != null) {
params.put("city", address.getCity());
}
if (address.getCounty() != null) {
params.put("county", address.getCounty());
}
if (address.getState() != null) {
params.put("state", address.getState());
}
if (address.getCountry() != null) {
params.put("country", address.getCountry());
}
if (address.getPostalCode() != null) {
params.put("postalcode", address.getPostalCode());
}
params.put("limit", "10");
NominatimResult[] results = nominatimClient.executeRequest(
config.getSearchUrl(), params, NominatimResult[].class);
return Arrays.asList(results);
} catch (Exception e) {
throw new NominatimException("Error searching by address components", e);
}
}
public List<NominatimResult> lookupObjects(List<String> osmIds) throws NominatimException {
try {
if (osmIds == null || osmIds.isEmpty()) {
return Collections.emptyList();
}
// Join OSM IDs (e.g., "N123,R456,W789")
String osmIdsParam = String.join(",", osmIds);
Map<String, String> params = new HashMap<>();
params.put("osm_ids", osmIdsParam);
params.put("limit", String.valueOf(osmIds.size()));
NominatimResult[] results = nominatimClient.executeRequest(
config.getLookupUrl(), params, NominatimResult[].class);
return Arrays.asList(results);
} catch (Exception e) {
throw new NominatimException("Error looking up OSM objects", e);
}
}
public BoundingBox getBoundingBox(String query, String countryCode) throws NominatimException {
List<NominatimResult> results = forwardGeocode(
ForwardGeocodeRequest.builder()
.query(query)
.countryCode(countryCode)
.limit(1)
.build()
);
if (results.isEmpty()) {
throw new NominatimException("No results found for query: " + query);
}
return results.get(0).getBoundingbox();
}
}
Place Search Service
@Service
public class NominatimPlaceService {
private final NominatimGeocodingService geocodingService;
public NominatimPlaceService(NominatimGeocodingService geocodingService) {
this.geocodingService = geocodingService;
}
public List<NominatimResult> findNearbyPlaces(double latitude, double longitude,
PlaceType placeType, double radius)
throws NominatimException {
// Use reverse geocoding with specific parameters for nearby places
NominatimResult location = geocodingService.reverseGeocode(latitude, longitude, 18);
// Then search for specific place types in the area
ForwardGeocodeRequest request = ForwardGeocodeRequest.builder()
.query(placeType.getSearchQuery())
.limit(20)
.build();
return geocodingService.forwardGeocode(request);
}
public List<NominatimResult> searchPlacesByCategory(String category, String countryCode, int limit)
throws NominatimException {
ForwardGeocodeRequest request = ForwardGeocodeRequest.builder()
.query(category)
.countryCode(countryCode)
.limit(limit)
.build();
return geocodingService.forwardGeocode(request);
}
public List<NominatimResult> findPlacesInBoundingBox(double minLat, double minLon,
double maxLat, double maxLon,
String query) throws NominatimException {
String viewbox = minLon + "," + maxLat + "," + maxLon + "," + minLat; // left, top, right, bottom
ForwardGeocodeRequest request = ForwardGeocodeRequest.builder()
.query(query)
.viewbox(viewbox)
.bounded(true)
.limit(50)
.build();
return geocodingService.forwardGeocode(request);
}
}
Step 4: Data Models
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ForwardGeocodeRequest {
@NotBlank
private String query;
private String countryCode; // ISO 3166-1 alpha-2 code (e.g., "us", "gb")
private int limit = 10;
private String viewbox; // "min_lon,min_lat,max_lon,max_lat"
private boolean bounded = false;
private String language = "en";
public static class ForwardGeocodeRequestBuilder {
private int limit = 10;
private String language = "en";
private boolean bounded = false;
}
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class NominatimResult {
@JsonProperty("place_id")
private Long placeId;
@JsonProperty("osm_type")
private String osmType; // node, way, relation
@JsonProperty("osm_id")
private Long osmId;
private Double lat;
private Double lon;
@JsonProperty("display_name")
private String displayName;
@JsonProperty("class")
private String placeClass; // amenity, shop, tourism, etc.
private String type; // restaurant, hotel, etc.
private Double importance;
private String icon;
@JsonProperty("address")
private AddressDetails address;
@JsonProperty("boundingbox")
private List<Double> boundingbox;
@JsonProperty("geojson")
private Map<String, Object> geoJson;
// Helper methods
public BoundingBox getBoundingBox() {
if (boundingbox != null && boundingbox.size() == 4) {
return new BoundingBox(
boundingbox.get(0), // min lat
boundingbox.get(1), // max lat
boundingbox.get(2), // min lon
boundingbox.get(3) // max lon
);
}
return null;
}
public String getOsmIdentifier() {
return (osmType != null ? osmType.charAt(0) : 'N') + osmId.toString();
}
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class AddressDetails {
private String road;
private String neighbourhood;
private String suburb;
private String city;
private String county;
private String state;
private String postcode;
private String country;
@JsonProperty("country_code")
private String countryCode;
private String town;
private String village;
private String municipality;
// Additional address components
private String house_number;
private String pedestrian;
private String quarter;
private String city_district;
private String region;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BoundingBox {
private Double minLatitude;
private Double maxLatitude;
private Double minLongitude;
private Double maxLongitude;
public String toString() {
return minLongitude + "," + minLatitude + "," + maxLongitude + "," + maxLatitude;
}
}
@Data
@Builder
public class AddressComponents {
private String street;
private String city;
private String county;
private String state;
private String country;
private String postalCode;
private String houseNumber;
}
public enum PlaceType {
RESTAURANT("restaurant"),
HOTEL("hotel"),
CAFE("cafe"),
BANK("bank"),
HOSPITAL("hospital"),
PHARMACY("pharmacy"),
SUPERMARKET("supermarket"),
MALL("mall"),
GAS_STATION("fuel"),
PARKING("parking"),
BUS_STOP("bus_stop"),
TRAIN_STATION("train_station"),
AIRPORT("airport"),
UNIVERSITY("university"),
SCHOOL("school"),
MUSEUM("museum"),
CINEMA("cinema");
private final String searchQuery;
PlaceType(String searchQuery) {
this.searchQuery = searchQuery;
}
public String getSearchQuery() {
return searchQuery;
}
}
Step 5: Caching Configuration
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager(NominatimConfig nominatimConfig) {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(nominatimConfig.getCacheExpiryHours(), TimeUnit.HOURS)
.maximumSize(nominatimConfig.getCacheSize())
.recordStats());
return cacheManager;
}
}
@Service
public class GeocodingCacheService {
@CacheEvict(value = {"forwardGeocoding", "reverseGeocoding"}, allEntries = true)
public void clearAllGeocodingCache() {
// This method will clear all cached geocoding results when called
}
@CacheEvict(value = "forwardGeocoding", key = "#query + '|' + #countryCode + '|' + #limit")
public void evictForwardGeocodingCache(String query, String countryCode, int limit) {
// Specific cache eviction for forward geocoding
}
}
Step 6: REST API Controllers
@RestController
@RequestMapping("/api/nominatim")
@Validated
@Slf4j
public class NominatimController {
@Autowired
private NominatimGeocodingService geocodingService;
@Autowired
private NominatimPlaceService placeService;
@Autowired
private GeocodingCacheService cacheService;
@PostMapping("/geocode/forward")
public ResponseEntity<?> forwardGeocode(@Valid @RequestBody ForwardGeocodeRequest request) {
try {
List<NominatimResult> results = geocodingService.forwardGeocode(request);
return ResponseEntity.ok(new GeocodingResponse(true, "Success", results));
} catch (NominatimException e) {
log.error("Forward geocoding error for query: {}", request.getQuery(), e);
return ResponseEntity.badRequest().body(
new ErrorResponse("GEOCODING_ERROR", e.getMessage())
);
}
}
@GetMapping("/geocode/reverse")
public ResponseEntity<?> reverseGeocode(
@RequestParam @DecimalMin("-90") @DecimalMax("90") double lat,
@RequestParam @DecimalMin("-180") @DecimalMax("180") double lon,
@RequestParam(defaultValue = "18") @Min(0) @Max(18) int zoom) {
try {
NominatimResult result = geocodingService.reverseGeocode(lat, lon, zoom);
return ResponseEntity.ok(new GeocodingResponse(true, "Success", List.of(result)));
} catch (NominatimException e) {
log.error("Reverse geocoding error for coordinates: {},{}", lat, lon, e);
return ResponseEntity.badRequest().body(
new ErrorResponse("REVERSE_GEOCODING_ERROR", e.getMessage())
);
}
}
@PostMapping("/geocode/address")
public ResponseEntity<?> geocodeByAddress(@Valid @RequestBody AddressComponents address) {
try {
List<NominatimResult> results = geocodingService.searchByAddress(address);
return ResponseEntity.ok(new GeocodingResponse(true, "Success", results));
} catch (NominatimException e) {
log.error("Address-based geocoding error", e);
return ResponseEntity.badRequest().body(
new ErrorResponse("ADDRESS_GEOCODING_ERROR", e.getMessage())
);
}
}
@GetMapping("/places/nearby")
public ResponseEntity<?> findNearbyPlaces(
@RequestParam double lat,
@RequestParam double lon,
@RequestParam(defaultValue = "RESTAURANT") PlaceType placeType,
@RequestParam(defaultValue = "1000") double radius) {
try {
List<NominatimResult> results = placeService.findNearbyPlaces(lat, lon, placeType, radius);
return ResponseEntity.ok(new GeocodingResponse(true, "Success", results));
} catch (NominatimException e) {
log.error("Nearby places search error", e);
return ResponseEntity.badRequest().body(
new ErrorResponse("NEARBY_PLACES_ERROR", e.getMessage())
);
}
}
@GetMapping("/places/category")
public ResponseEntity<?> searchByCategory(
@RequestParam String category,
@RequestParam(required = false) String countryCode,
@RequestParam(defaultValue = "10") int limit) {
try {
List<NominatimResult> results = placeService.searchPlacesByCategory(category, countryCode, limit);
return ResponseEntity.ok(new GeocodingResponse(true, "Success", results));
} catch (NominatimException e) {
log.error("Category search error", e);
return ResponseEntity.badRequest().body(
new ErrorResponse("CATEGORY_SEARCH_ERROR", e.getMessage())
);
}
}
@PostMapping("/cache/clear")
public ResponseEntity<?> clearCache() {
cacheService.clearAllGeocodingCache();
return ResponseEntity.ok("Cache cleared successfully");
}
}
// Response DTOs
@Data
@AllArgsConstructor
@NoArgsConstructor
class GeocodingResponse {
private boolean success;
private String message;
private List<NominatimResult> results;
private int count;
public GeocodingResponse(boolean success, String message, List<NominatimResult> results) {
this.success = success;
this.message = message;
this.results = results;
this.count = results != null ? results.size() : 0;
}
}
@Data
@AllArgsConstructor
class ErrorResponse {
private String errorCode;
private String message;
private LocalDateTime timestamp;
public ErrorResponse(String errorCode, String message) {
this.errorCode = errorCode;
this.message = message;
this.timestamp = LocalDateTime.now();
}
}
Step 7: Exception Handling
public class NominatimException extends Exception {
public NominatimException(String message) {
super(message);
}
public NominatimException(String message, Throwable cause) {
super(message, cause);
}
}
@ControllerAdvice
public class NominatimExceptionHandler {
@ExceptionHandler(NominatimException.class)
public ResponseEntity<ErrorResponse> handleNominatimException(NominatimException ex) {
ErrorResponse error = new ErrorResponse("NOMINATIM_ERROR", ex.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ErrorResponse> handleValidationException(ConstraintViolationException ex) {
String message = ex.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(", "));
ErrorResponse error = new ErrorResponse("VALIDATION_ERROR", message);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
}
Step 8: Configuration File
application.yml:
nominatim: base-url: "https://nominatim.openstreetmap.org" user-agent: "YourApp/1.0 ([email protected])" email: "[email protected]" # Optional but recommended timeout: 30000 cache-size: 1000 cache-expiry-hours: 24 rate-limit-enabled: true requests-per-second: 1 spring: cache: type: caffeine logging: level: com.yourcompany.nominatim: DEBUG
Step 9: Usage Examples
@Service
public class ExampleUsageService {
@Autowired
private NominatimGeocodingService geocodingService;
public void demonstrateUsage() {
try {
// Forward geocoding - address to coordinates
ForwardGeocodeRequest request = ForwardGeocodeRequest.builder()
.query("1600 Amphitheatre Parkway, Mountain View, CA")
.countryCode("us")
.limit(5)
.build();
List<NominatimResult> results = geocodingService.forwardGeocode(request);
// Reverse geocoding - coordinates to address
NominatimResult reverseResult = geocodingService.reverseGeocode(37.4220, -122.0841, 18);
// Address component search
AddressComponents address = AddressComponents.builder()
.street("Main Street")
.city("New York")
.state("NY")
.country("United States")
.build();
List<NominatimResult> addressResults = geocodingService.searchByAddress(address);
} catch (NominatimException e) {
// Handle exception
}
}
}
Best Practices
- Respect Rate Limits: Nominatim requires max 1 request per second
- Use Caching: Cache results to reduce API calls and improve performance
- Set User Agent: Always include a meaningful User-Agent header
- Provide Email: Include email for usage policy compliance
- Handle Errors Gracefully: Implement robust error handling
- Validate Inputs: Validate coordinates and address inputs
- Use Appropriate Zoom Levels: Choose correct zoom for reverse geocoding
- Respect OpenStreetMap: Follow OSM's usage guidelines and attribution requirements
This comprehensive integration provides a robust foundation for using OpenStreetMap Nominatim in Java applications, enabling powerful geocoding and location-based services while respecting the service's usage policies.
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.