diff --git a/schema/building.fbs b/schema/building.fbs
new file mode 100644
index 0000000..05146e6
--- /dev/null
+++ b/schema/building.fbs
@@ -0,0 +1,17 @@
+namespace com.dedicatedcode.paikka.flatbuffers;
+
+table Geometry {
+ data:[ubyte]; // WKB binary
+}
+table Building {
+ id: long;
+ name: string; // building type (residential, commercial, etc.)
+ code: string; // always "building"
+ geometry: Geometry;
+}
+
+table BuildingList {
+ buildings: [Building];
+}
+
+root_type BuildingList;
\ No newline at end of file
diff --git a/scripts/filter_osm.sh b/scripts/filter_osm.sh
index 492e638..b876351 100755
--- a/scripts/filter_osm.sh
+++ b/scripts/filter_osm.sh
@@ -54,8 +54,8 @@ echo "Input file: $INPUT_FILE"
echo "Output file: $OUTPUT_FILE"
echo ""
osmium tags-filter "$INPUT_FILE" \
- nwr/amenity!=bench,drinking_water,waste_basket,bicycle_parking,vending_machine,parking_entrance,fire_hydrant,recycling \
- nwr/emergency!=fire_hydrant,defibrillator \
+ nwr/amenity!=bench,drinking_water,waste_basket,bicycle_parking,vending_machine,parking_entrance,fire_hydrant,recycling,post_box,atm,loading_ramp,parcel_locker,trolley_bay \
+ nwr/emergency!=fire_hydrant,defibrillator,fire_service_inlet \
nw/shop \
nw/tourism \
nw/leisure \
@@ -70,6 +70,7 @@ osmium tags-filter "$INPUT_FILE" \
w/building=yes,commercial,retail,industrial,office,apartments,residential \
r/boundary=administrative \
r/type=multipolygon \
+ n/addr:* \
-o "$OUTPUT_FILE" --overwrite
if [ $? -eq 0 ]; then
diff --git a/src/main/java/com/dedicatedcode/paikka/controller/AdminController.java b/src/main/java/com/dedicatedcode/paikka/controller/AdminController.java
index 5d8bf47..6146856 100644
--- a/src/main/java/com/dedicatedcode/paikka/controller/AdminController.java
+++ b/src/main/java/com/dedicatedcode/paikka/controller/AdminController.java
@@ -19,6 +19,7 @@
import com.dedicatedcode.paikka.service.ReverseGeocodingService;
import com.dedicatedcode.paikka.service.BoundaryService;
import com.dedicatedcode.paikka.service.MetadataService;
+import com.dedicatedcode.paikka.service.BuildingService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -44,11 +45,16 @@ public class AdminController {
private final ReverseGeocodingService reverseGeocodingService;
private final BoundaryService boundaryService;
private final MetadataService metadataService;
+ private final BuildingService buildingService;
- public AdminController(ReverseGeocodingService reverseGeocodingService, BoundaryService boundaryService, MetadataService metadataService) {
+ public AdminController(ReverseGeocodingService reverseGeocodingService,
+ BoundaryService boundaryService,
+ MetadataService metadataService,
+ BuildingService buildingService) {
this.reverseGeocodingService = reverseGeocodingService;
this.boundaryService = boundaryService;
this.metadataService = metadataService;
+ this.buildingService = buildingService;
}
@PostMapping(value = "/refresh-db", produces = "application/json")
@@ -64,6 +70,10 @@ public ResponseEntity> refreshDatabase() {
logger.info("Reloading boundaries database...");
boundaryService.reloadDatabase();
+ // Reload the building service (buildings database)
+ logger.info("Reloading buildings database...");
+ buildingService.reloadDatabase();
+
// Reload metadata
logger.info("Reloading metadata...");
metadataService.reload();
diff --git a/src/main/java/com/dedicatedcode/paikka/flatbuffers/Building.java b/src/main/java/com/dedicatedcode/paikka/flatbuffers/Building.java
new file mode 100644
index 0000000..601913e
--- /dev/null
+++ b/src/main/java/com/dedicatedcode/paikka/flatbuffers/Building.java
@@ -0,0 +1,58 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.dedicatedcode.paikka.flatbuffers;
+
+import java.nio.*;
+import java.lang.*;
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+public final class Building extends Table {
+ public static void ValidateVersion() { Constants.FLATBUFFERS_25_2_10(); }
+ public static Building getRootAsBuilding(ByteBuffer _bb) { return getRootAsBuilding(_bb, new Building()); }
+ public static Building getRootAsBuilding(ByteBuffer _bb, Building obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { __reset(_i, _bb); }
+ public Building __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public long id() { int o = __offset(4); return o != 0 ? bb.getLong(o + bb_pos) : 0L; }
+ public String name() { int o = __offset(6); return o != 0 ? __string(o + bb_pos) : null; }
+ public ByteBuffer nameAsByteBuffer() { return __vector_as_bytebuffer(6, 1); }
+ public ByteBuffer nameInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 6, 1); }
+ public String code() { int o = __offset(8); return o != 0 ? __string(o + bb_pos) : null; }
+ public ByteBuffer codeAsByteBuffer() { return __vector_as_bytebuffer(8, 1); }
+ public ByteBuffer codeInByteBuffer(ByteBuffer _bb) { return __vector_in_bytebuffer(_bb, 8, 1); }
+ public com.dedicatedcode.paikka.flatbuffers.Geometry geometry() { return geometry(new com.dedicatedcode.paikka.flatbuffers.Geometry()); }
+ public com.dedicatedcode.paikka.flatbuffers.Geometry geometry(com.dedicatedcode.paikka.flatbuffers.Geometry obj) { int o = __offset(10); return o != 0 ? obj.__assign(__indirect(o + bb_pos), bb) : null; }
+
+ public static int createBuilding(FlatBufferBuilder builder,
+ long id,
+ int nameOffset,
+ int codeOffset,
+ int geometryOffset) {
+ builder.startTable(4);
+ Building.addId(builder, id);
+ Building.addGeometry(builder, geometryOffset);
+ Building.addCode(builder, codeOffset);
+ Building.addName(builder, nameOffset);
+ return Building.endBuilding(builder);
+ }
+
+ public static void startBuilding(FlatBufferBuilder builder) { builder.startTable(4); }
+ public static void addId(FlatBufferBuilder builder, long id) { builder.addLong(0, id, 0L); }
+ public static void addName(FlatBufferBuilder builder, int nameOffset) { builder.addOffset(1, nameOffset, 0); }
+ public static void addCode(FlatBufferBuilder builder, int codeOffset) { builder.addOffset(2, codeOffset, 0); }
+ public static void addGeometry(FlatBufferBuilder builder, int geometryOffset) { builder.addOffset(3, geometryOffset, 0); }
+ public static int endBuilding(FlatBufferBuilder builder) {
+ int o = builder.endTable();
+ return o;
+ }
+
+ public static final class Vector extends BaseVector {
+ public Vector __assign(int _vector, int _element_size, ByteBuffer _bb) { __reset(_vector, _element_size, _bb); return this; }
+
+ public Building get(int j) { return get(new Building(), j); }
+ public Building get(Building obj, int j) { return obj.__assign(__indirect(__element(j), bb), bb); }
+ }
+}
+
diff --git a/src/main/java/com/dedicatedcode/paikka/flatbuffers/BuildingList.java b/src/main/java/com/dedicatedcode/paikka/flatbuffers/BuildingList.java
new file mode 100644
index 0000000..edf97f1
--- /dev/null
+++ b/src/main/java/com/dedicatedcode/paikka/flatbuffers/BuildingList.java
@@ -0,0 +1,49 @@
+// automatically generated by the FlatBuffers compiler, do not modify
+
+package com.dedicatedcode.paikka.flatbuffers;
+
+import java.nio.*;
+import java.lang.*;
+import java.util.*;
+import com.google.flatbuffers.*;
+
+@SuppressWarnings("unused")
+public final class BuildingList extends Table {
+ public static void ValidateVersion() { Constants.FLATBUFFERS_25_2_10(); }
+ public static BuildingList getRootAsBuildingList(ByteBuffer _bb) { return getRootAsBuildingList(_bb, new BuildingList()); }
+ public static BuildingList getRootAsBuildingList(ByteBuffer _bb, BuildingList obj) { _bb.order(ByteOrder.LITTLE_ENDIAN); return (obj.__assign(_bb.getInt(_bb.position()) + _bb.position(), _bb)); }
+ public void __init(int _i, ByteBuffer _bb) { __reset(_i, _bb); }
+ public BuildingList __assign(int _i, ByteBuffer _bb) { __init(_i, _bb); return this; }
+
+ public com.dedicatedcode.paikka.flatbuffers.Building buildings(int j) { return buildings(new com.dedicatedcode.paikka.flatbuffers.Building(), j); }
+ public com.dedicatedcode.paikka.flatbuffers.Building buildings(com.dedicatedcode.paikka.flatbuffers.Building obj, int j) { int o = __offset(4); return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null; }
+ public int buildingsLength() { int o = __offset(4); return o != 0 ? __vector_len(o) : 0; }
+ public com.dedicatedcode.paikka.flatbuffers.Building.Vector buildingsVector() { return buildingsVector(new com.dedicatedcode.paikka.flatbuffers.Building.Vector()); }
+ public com.dedicatedcode.paikka.flatbuffers.Building.Vector buildingsVector(com.dedicatedcode.paikka.flatbuffers.Building.Vector obj) { int o = __offset(4); return o != 0 ? obj.__assign(__vector(o), 4, bb) : null; }
+
+ public static int createBuildingList(FlatBufferBuilder builder,
+ int buildingsOffset) {
+ builder.startTable(1);
+ BuildingList.addBuildings(builder, buildingsOffset);
+ return BuildingList.endBuildingList(builder);
+ }
+
+ public static void startBuildingList(FlatBufferBuilder builder) { builder.startTable(1); }
+ public static void addBuildings(FlatBufferBuilder builder, int buildingsOffset) { builder.addOffset(0, buildingsOffset, 0); }
+ public static int createBuildingsVector(FlatBufferBuilder builder, int[] data) { builder.startVector(4, data.length, 4); for (int i = data.length - 1; i >= 0; i--) builder.addOffset(data[i]); return builder.endVector(); }
+ public static void startBuildingsVector(FlatBufferBuilder builder, int numElems) { builder.startVector(4, numElems, 4); }
+ public static int endBuildingList(FlatBufferBuilder builder) {
+ int o = builder.endTable();
+ return o;
+ }
+ public static void finishBuildingListBuffer(FlatBufferBuilder builder, int offset) { builder.finish(offset); }
+ public static void finishSizePrefixedBuildingListBuffer(FlatBufferBuilder builder, int offset) { builder.finishSizePrefixed(offset); }
+
+ public static final class Vector extends BaseVector {
+ public Vector __assign(int _vector, int _element_size, ByteBuffer _bb) { __reset(_vector, _element_size, _bb); return this; }
+
+ public BuildingList get(int j) { return get(new BuildingList(), j); }
+ public BuildingList get(BuildingList obj, int j) { return obj.__assign(__indirect(__element(j), bb), bb); }
+ }
+}
+
diff --git a/src/main/java/com/dedicatedcode/paikka/service/BuildingService.java b/src/main/java/com/dedicatedcode/paikka/service/BuildingService.java
new file mode 100644
index 0000000..82911c9
--- /dev/null
+++ b/src/main/java/com/dedicatedcode/paikka/service/BuildingService.java
@@ -0,0 +1,276 @@
+/*
+ * This file is part of paikka.
+ *
+ * Paikka is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 or
+ * any later version.
+ *
+ * Paikka is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU Affero General Public License for more details.
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Paikka. If not, see .
+ */
+
+package com.dedicatedcode.paikka.service;
+
+import com.dedicatedcode.paikka.config.PaikkaConfiguration;
+import com.dedicatedcode.paikka.flatbuffers.Geometry;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.io.WKBReader;
+import org.rocksdb.Options;
+import org.rocksdb.RocksDB;
+import org.rocksdb.RocksDBException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Service;
+
+import java.nio.ByteBuffer;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Service for querying building information from the buildings database.
+ */
+@Service
+@ConditionalOnProperty(name = "paikka.import-mode", havingValue = "false", matchIfMissing = true)
+public class BuildingService {
+
+ private static final Logger logger = LoggerFactory.getLogger(BuildingService.class);
+ private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();
+
+ private final PaikkaConfiguration config;
+ private final S2Helper s2Helper;
+ private RocksDB buildingsDb;
+
+ public BuildingService(PaikkaConfiguration config, S2Helper s2Helper) {
+ this.config = config;
+ this.s2Helper = s2Helper;
+ initializeRocksDB();
+ }
+
+ private void initializeRocksDB() {
+ if (buildingsDb != null) {
+ return;
+ }
+
+ try {
+ RocksDB.loadLibrary();
+ Path buildingsDbPath = Paths.get(config.getDataDir(), "buildings_shards");
+
+ // Check if the database directory exists
+ if (!buildingsDbPath.toFile().exists()) {
+ logger.warn("Buildings database not found at: {}", buildingsDbPath);
+ return;
+ }
+
+ Options options = new Options().setCreateIfMissing(false);
+ this.buildingsDb = RocksDB.openReadOnly(options, buildingsDbPath.toString());
+ logger.info("Successfully initialized RocksDB for buildings");
+ } catch (Exception e) {
+ logger.warn("Failed to initialize RocksDB for buildings: {}", e.getMessage());
+ this.buildingsDb = null;
+ }
+ }
+
+ public synchronized void reloadDatabase() {
+ logger.info("Reloading buildings database...");
+
+ if (buildingsDb != null) {
+ try {
+ buildingsDb.close();
+ logger.info("Closed existing buildings database connection");
+ } catch (Exception e) {
+ logger.warn("Error closing existing buildings database: {}", e.getMessage());
+ }
+ buildingsDb = null;
+ }
+
+ initializeRocksDB();
+
+ if (buildingsDb != null) {
+ logger.info("Buildings database reloaded successfully");
+ } else {
+ logger.warn("Buildings database reload completed but database is not available");
+ }
+ }
+
+ /**
+ * Get building information for a point (lat, lon) by checking if it is contained in a building.
+ * This searches in the given shard and surrounding shards if needed.
+ */
+ public BuildingInfo getBuildingInfo(double lat, double lon) {
+ if (buildingsDb == null) {
+ logger.debug("Buildings database not initialized");
+ return null;
+ }
+
+ try {
+ Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(lon, lat));
+ long centerShardId = s2Helper.getShardId(lat, lon);
+ List shardsToSearch = new ArrayList<>();
+ shardsToSearch.add(centerShardId);
+ shardsToSearch.addAll(s2Helper.getNeighborShards(centerShardId));
+
+ for (Long shardId : shardsToSearch) {
+ byte[] key = s2Helper.longToByteArray(shardId);
+ byte[] data = buildingsDb.get(key);
+
+ if (data != null) {
+ ByteBuffer buffer = ByteBuffer.wrap(data);
+ com.dedicatedcode.paikka.flatbuffers.BuildingList buildingList = com.dedicatedcode.paikka.flatbuffers.BuildingList.getRootAsBuildingList(buffer);
+ for (int i = 0; i < buildingList.buildingsLength(); i++) {
+ com.dedicatedcode.paikka.flatbuffers.Building building = buildingList.buildings(i);
+ if (building != null && building.geometry() != null) {
+ Geometry geometryFb = building.geometry();
+ if (geometryFb.dataLength() > 0) {
+ byte[] wkbData = new byte[geometryFb.dataLength()];
+ for (int j = 0; j < geometryFb.dataLength(); j++) {
+ wkbData[j] = (byte) geometryFb.data(j);
+ }
+ try {
+ WKBReader wkbReader = new WKBReader();
+ org.locationtech.jts.geom.Geometry jtsGeometry = wkbReader.read(wkbData);
+ if (jtsGeometry.intersects(point)) {
+ return decodeBuildingInfo(building);
+ }
+ } catch (Exception e) {
+ logger.warn("Failed to read geometry for building {}", building.id(), e);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ logger.debug("Building containing point ({}, {}) not found in buildings database", lon, lat);
+ return null;
+
+ } catch (RocksDBException e) {
+ logger.error("RocksDB error while querying building info for point ({}, {})", lon, lat, e);
+ return null;
+ } catch (Exception e) {
+ logger.error("Error while querying building info for point ({}, {})", lon, lat, e);
+ return null;
+ }
+ }
+
+ private BuildingInfo decodeBuildingInfo(com.dedicatedcode.paikka.flatbuffers.Building building) {
+ try {
+ BuildingInfo info = new BuildingInfo();
+ info.setOsmId(building.id());
+ info.setLevel(100);
+ info.setName(building.name());
+ info.setCode(building.code());
+ info.setType(building.name());
+
+ // Decode geometry if present
+ if (building.geometry() != null) {
+ Geometry geometry = building.geometry();
+ if (geometry.dataLength() > 0) {
+ byte[] wkbData = new byte[geometry.dataLength()];
+ for (int i = 0; i < geometry.dataLength(); i++) {
+ wkbData[i] = (byte) geometry.data(i);
+ }
+
+ try {
+ WKBReader wkbReader = new WKBReader();
+ org.locationtech.jts.geom.Geometry jtsGeometry = wkbReader.read(wkbData);
+ info.setGeometry(jtsGeometry);
+ info.setBoundaryWkb(wkbData);
+ } catch (Exception e) {
+ logger.warn("Failed to decode geometry for building OSM ID {}: {}", building.id(), e.getMessage());
+ }
+ }
+ }
+
+ return info;
+ } catch (Exception e) {
+ logger.error("Failed to decode building info for OSM ID {}", building.id(), e);
+ return null;
+ }
+ }
+
+ /**
+ * Check if the buildings database is available.
+ */
+ public boolean isAvailable() {
+ return buildingsDb != null;
+ }
+
+ /**
+ * Represents building information retrieved from the buildings database.
+ */
+ public static class BuildingInfo {
+ private long osmId;
+ private int level;
+ private String name;
+ private String code;
+ private String type;
+ private org.locationtech.jts.geom.Geometry geometry;
+ private byte[] boundaryWkb;
+
+ public long getOsmId() {
+ return osmId;
+ }
+
+ public void setOsmId(long osmId) {
+ this.osmId = osmId;
+ }
+
+ public int getLevel() {
+ return level;
+ }
+
+ public void setLevel(int level) {
+ this.level = level;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getCode() {
+ return code;
+ }
+
+ public void setCode(String code) {
+ this.code = code;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public org.locationtech.jts.geom.Geometry getGeometry() {
+ return geometry;
+ }
+
+ public void setGeometry(org.locationtech.jts.geom.Geometry geometry) {
+ this.geometry = geometry;
+ }
+
+ public byte[] getBoundaryWkb() {
+ return boundaryWkb;
+ }
+
+ public void setBoundaryWkb(byte[] boundaryWkb) {
+ this.boundaryWkb = boundaryWkb;
+ }
+ }
+}
diff --git a/src/main/java/com/dedicatedcode/paikka/service/ReverseGeocodingService.java b/src/main/java/com/dedicatedcode/paikka/service/ReverseGeocodingService.java
index 38623e3..955c90b 100644
--- a/src/main/java/com/dedicatedcode/paikka/service/ReverseGeocodingService.java
+++ b/src/main/java/com/dedicatedcode/paikka/service/ReverseGeocodingService.java
@@ -50,11 +50,13 @@ public class ReverseGeocodingService {
private final PaikkaConfiguration config;
private final S2Helper s2Helper;
+ private final BuildingService buildingService;
private RocksDB shardsDb;
- public ReverseGeocodingService(PaikkaConfiguration config, S2Helper s2Helper) {
+ public ReverseGeocodingService(PaikkaConfiguration config, S2Helper s2Helper, BuildingService buildingService) {
this.config = config;
this.s2Helper = s2Helper;
+ this.buildingService = buildingService;
initializeRocksDB();
}
@@ -176,10 +178,15 @@ public List findNearbyPOIs(double lat, double lon, String lang, int
// Find the closest POIs up to the limit
List closestPOIs = findClosestPOIs(allPOIs, lat, lon, limit);
- // Convert POIs to response format
+ // Convert POIs to response format and enhance with building info if needed
List results = new ArrayList<>();
for (POIData poi : closestPOIs) {
- results.add(convertPOIToResponse(poi, lat, lon, lang));
+ POIResponse response = convertPOIToResponse(poi, lat, lon, lang);
+
+ // Enhance with building information if this is a building or related to a building
+ enhanceWithBuildingInfo(response, poi);
+
+ results.add(response);
}
logger.debug("Final result: {} POIs found from {} searched shards", results.size(), searchedShards.size());
@@ -417,6 +424,35 @@ private POIResponse convertPOIToResponse(POIData poi, double queryLat, double qu
return response;
}
+
+ private void enhanceWithBuildingInfo(POIResponse response, POIData poi) {
+ BuildingService.BuildingInfo buildingInfo = buildingService.getBuildingInfo(poi.lat(), poi.lon());
+ if (buildingInfo != null) {
+ // Update type and subtype with building information
+ if (buildingInfo.getType() != null) {
+ response.setType("building");
+ }
+ // For subtype, we can use building name or code
+ if (buildingInfo.getName() != null) {
+ response.setSubtype(buildingInfo.getName());
+ } else if (buildingInfo.getCode() != null) {
+ response.setSubtype(buildingInfo.getCode());
+ }
+
+ // If building has boundary data, set it in the response
+ if (buildingInfo.getBoundaryWkb() != null && buildingInfo.getBoundaryWkb().length > 0) {
+ try {
+ WKBReader wkbReader = new WKBReader();
+ Geometry geometry = wkbReader.read(buildingInfo.getBoundaryWkb());
+ GeoJsonGeometry geoJsonGeometry = convertJtsToGeoJson(geometry);
+ response.setBoundary(geoJsonGeometry);
+ logger.debug("Enhanced POI {} with building boundary", poi.id());
+ } catch (Exception e) {
+ logger.warn("Failed to convert building boundary to GeoJSON for POI {}: {}", poi.id(), e.getMessage());
+ }
+ }
+ }
+ }
private record POIData(long id, float lat, float lon, String type, String subtype, Map names,
diff --git a/src/main/java/com/dedicatedcode/paikka/service/importer/ImportService.java b/src/main/java/com/dedicatedcode/paikka/service/importer/ImportService.java
index b6de56a..968315e 100644
--- a/src/main/java/com/dedicatedcode/paikka/service/importer/ImportService.java
+++ b/src/main/java/com/dedicatedcode/paikka/service/importer/ImportService.java
@@ -67,9 +67,11 @@ public class ImportService {
private final PaikkaConfiguration config;
private final Map tagCache = new ConcurrentHashMap<>(1000);
+
private final int fileReadWindowSize;
private final AtomicLong sequence = new AtomicLong(0);
+ private final AtomicLong buildingSequence = new AtomicLong(0);
public ImportService(S2Helper s2Helper, GeometrySimplificationService geometrySimplificationService, PaikkaConfiguration config) {
this.s2Helper = s2Helper;
@@ -101,7 +103,10 @@ public void importData(String pbfFilePath, String dataDir) throws Exception {
Path shardsDbPath = dataDirectory.resolve("poi_shards");
Path boundariesDbPath = dataDirectory.resolve("boundaries");
+ Path buildingsDbPath = dataDirectory.resolve("buildings_shards");
+ Path appendBuildingDbPath = dataDirectory.resolve("tmp/append_building");
Path gridIndexDbPath = dataDirectory.resolve("tmp/grid_index");
+ Path buildingGridIndexDbPath = dataDirectory.resolve("tmp/building_grid_index");
Path appendDbPath = dataDirectory.resolve("tmp/append_poi");
Path nodeCacheDbPath = dataDirectory.resolve("tmp/node_cache");
Path wayIndexDbPath = dataDirectory.resolve("tmp/way_index");
@@ -112,7 +117,10 @@ public void importData(String pbfFilePath, String dataDir) throws Exception {
cleanupDatabase(shardsDbPath);
cleanupDatabase(boundariesDbPath);
+ cleanupDatabase(buildingsDbPath);
+ cleanupDatabase(appendBuildingDbPath);
cleanupDatabase(gridIndexDbPath);
+ cleanupDatabase(buildingGridIndexDbPath);
cleanupDatabase(nodeCacheDbPath);
cleanupDatabase(wayIndexDbPath);
cleanupDatabase(boundaryWayIndexDbPath);
@@ -184,7 +192,10 @@ public void importData(String pbfFilePath, String dataDir) throws Exception {
try (RocksDB shardsDb = RocksDB.open(poiShardsOpts, shardsDbPath.toString());
RocksDB boundariesDb = RocksDB.open(poiShardsOpts, boundariesDbPath.toString());
+ RocksDB buildingsDb = RocksDB.open(poiShardsOpts, buildingsDbPath.toString());
+ RocksDB appendBuildingDb = RocksDB.open(appendOpts, appendBuildingDbPath.toString());
RocksDB gridIndexDb = RocksDB.open(gridOpts, gridIndexDbPath.toString());
+ RocksDB buildingGridIndexDb = RocksDB.open(gridOpts, buildingGridIndexDbPath.toString());
RocksDB nodeCache = RocksDB.open(nodeOpts, nodeCacheDbPath.toString());
RocksDB wayIndexDb = RocksDB.open(wayIndexOpts, wayIndexDbPath.toString());
RocksDB neededBoundaryWaysDb = RocksDB.open(wayIndexOpts, boundaryWayIndexDbPath.toString());
@@ -205,22 +216,27 @@ public void importData(String pbfFilePath, String dataDir) throws Exception {
// PASS 2: Nodes Cache, Boundaries, POIs
stats.printPhaseHeader("PASS 2: Nodes Cache, Boundaries, POIs");
long pass2Start = System.currentTimeMillis();
- stats.setCurrentPhase(3, "1.1.3: Caching node coordinates");
+ stats.setCurrentPhase(3, "1.2: Caching node coordinates");
cacheNeededNodeCoordinates(pbfFile, neededNodesDb, nodeCache, stats);
- stats.setCurrentPhase(4, "1.2: Processing administrative boundaries");
+ stats.setCurrentPhase(4, "1.3: Processing administrative boundaries");
processAdministrativeBoundariesFromIndex(relIndexDb, nodeCache, wayIndexDb, gridIndexDb, boundariesDb, stats);
- stats.setCurrentPhase(5, "2.1: Processing POIs & Sharding");
+
+ stats.setCurrentPhase(5, "1.4: Processing building boundaries");
+ processBuildingBoundariesFromIndex(poiIndexDb, nodeCache, wayIndexDb, buildingGridIndexDb, appendBuildingDb, stats);
+
pass2PoiShardingFromIndex(nodeCache, wayIndexDb, appendDb, boundariesDb, poiIndexDb, gridIndexDb, stats);
- stats.setCurrentPhase(6, "2.2: Compacting POIs");
+ stats.setCurrentPhase(8, "2.2: Compacting POIs");
compactShards(appendDb, shardsDb, stats);
+ stats.setCurrentPhase(9, "2.3: Compacting Buildings");
+ compactBuildingShards(appendBuildingDb, buildingsDb, stats);
stats.stop();
stats.printPhaseSummary("PASS 2", pass2Start);
shardsDb.compactRange();
boundariesDb.compactRange();
-
+ buildingsDb.compactRange();
stats.setTotalTime(System.currentTimeMillis() - totalStartTime);
@@ -228,7 +244,10 @@ public void importData(String pbfFilePath, String dataDir) throws Exception {
recordSizeMetrics(stats,
shardsDbPath,
boundariesDbPath,
+ buildingsDbPath,
+ appendBuildingDbPath,
gridIndexDbPath,
+ buildingGridIndexDbPath,
nodeCacheDbPath,
wayIndexDbPath,
boundaryWayIndexDbPath,
@@ -327,7 +346,9 @@ private void pass1DiscoveryAndIndexing(Path pbfFile, RocksDB wayIndexDb, RocksDB
try {
if (type == EntityType.Node) {
OsmNode node = (OsmNode) container.getEntity();
- if (isPoi(node)) {
+ boolean isAddressNode = hasAddressTags(node);
+
+ if (isPoi(node) || isAddressNode) {
PoiIndexRec rec = buildPoiIndexRecFromEntity(node);
rec.lat = node.getLatitude();
rec.lon = node.getLongitude();
@@ -335,12 +356,16 @@ private void pass1DiscoveryAndIndexing(Path pbfFile, RocksDB wayIndexDb, RocksDB
poiWriter.put(key, encodePoiIndexRec(rec));
neededWriter.put(s2Helper.longToByteArray(node.getId()), ONE);
stats.incrementNodesFound();
+ if (isAddressNode) {
+ stats.incrementAddressNodesFound();
+ }
}
} else if (type == EntityType.Way) {
OsmWay way = (OsmWay) container.getEntity();
boolean isPoi = isPoi(way);
boolean isAdmin = isAdministrativeBoundaryWay(way);
+
if (isPoi || isAdmin) {
stats.incrementWaysProcessed();
int n = way.getNumberOfNodes();
@@ -350,15 +375,18 @@ private void pass1DiscoveryAndIndexing(Path pbfFile, RocksDB wayIndexDb, RocksDB
nodeIds[j] = nid;
neededWriter.put(s2Helper.longToByteArray(nid), ONE);
}
- wayWriter.put(s2Helper.longToByteArray(way.getId()), s2Helper.longArrayToByteArray(nodeIds));
+ wayWriter.put(s2Helper.longToByteArray(way.getId()),
+ s2Helper.longArrayToByteArray(nodeIds));
+
if (isPoi) {
PoiIndexRec rec = buildPoiIndexRecFromEntity(way);
- // lat/lon remain NaN for ways — resolved in Pass 2 reader
byte[] key = buildPoiKey((byte) 'W', way.getId());
poiWriter.put(key, encodePoiIndexRec(rec));
+ if ("building".equals(rec.type)) {
+ stats.incrementBuildingsFound();
+ }
}
}
-
} else if (type == EntityType.Relation) {
OsmRelation relation = (OsmRelation) container.getEntity();
if (isAdministrativeBoundary(relation)) {
@@ -417,11 +445,17 @@ private void pass2PoiShardingFromIndex(RocksDB nodeCache,
try (ExecutorService executor = createExecutorService(numReaders); ReadOptions ro = new ReadOptions().setReadaheadSize(8 * 1024 * 1024)) {
com.github.benmanes.caffeine.cache.Cache globalBoundaryCache = Caffeine.newBuilder().maximumSize(1000).recordStats().build();
ThreadLocal hierarchyCacheThreadLocal = ThreadLocal.withInitial(() -> new HierarchyCache(boundariesDb, gridIndexDb, s2Helper, globalBoundaryCache));
+
+ stats.setCurrentPhase(6, "1.5: Preparing POI Sharding");
long total = 0;
try (RocksIterator it = poiIndexDb.newIterator(ro)) {
- for (it.seekToFirst(); it.isValid(); it.next()) total++;
+ for (it.seekToFirst(); it.isValid(); it.next()) {
+ total++;
+ stats.incrementPoiIndexEntriesScanned();
+ }
}
+ stats.setCurrentPhase(7, "2.1: Processing POIs & Sharding");
long step = Math.max(1, total / numReaders);
List splitKeys = new ArrayList<>();
try (RocksIterator it = poiIndexDb.newIterator(ro)) {
@@ -453,6 +487,15 @@ private void pass2PoiShardingFromIndex(RocksDB nodeCache,
rec.kind = kind;
rec.id = id;
+ // Solution: The old workaround is removed. Enrichment is done at query time.
+ // We no longer try to enrich address nodes during import.
+
+ // Buildings are processed in a separate phase and not included as point POIs.
+ if ("building".equals(rec.type)) {
+ it.next();
+ continue;
+ }
+
// For way POIs, lat/lon is NaN — resolve from nodeCache/wayIndexDb
byte[] cacheWayNodes = null;
if (Double.isNaN(rec.lat) && kind == 'W') {
@@ -486,8 +529,6 @@ private void pass2PoiShardingFromIndex(RocksDB nodeCache,
}
- // ─── Worker threads: hierarchy resolution + sharding (unchanged) ───
-
for (int i = 0; i < numReaders; i++) {
executor.submit(() -> {
stats.incrementActiveThreads();
@@ -508,7 +549,6 @@ private void pass2PoiShardingFromIndex(RocksDB nodeCache,
double lon = rec.lon;
byte[] boundaryWkb = null;
- // For way POIs, still compute boundary WKB geometry
if (rec.kind == 'W') {
List coords = buildCoordinatesFromWay(nodeCache, item.cachedWayNodes);
if (coords != null && coords.size() >= 3) {
@@ -575,11 +615,6 @@ private void pass2PoiShardingFromIndex(RocksDB nodeCache,
}
}
- /**
- * Sorts a chunk of POI items by S2CellId (Hilbert curve order) for spatial
- * locality, then emits to the processing queue in batches.
- * Memory stays bounded: only one chunk is in memory at a time.
- */
private void sortAndEmitChunk(List chunk, BlockingQueue> queue, ImportStatistics stats) throws InterruptedException {
chunk.sort((a, b) -> Long.compareUnsigned(a.s2SortKey, b.s2SortKey));
@@ -641,13 +676,11 @@ private byte[] resolveWayCenter(PoiIndexRec rec, RocksDB nodeCache, RocksDB wayI
private void cacheNeededNodeCoordinates(Path pbfFile, RocksDB neededNodesDb, RocksDB nodeCache, ImportStatistics stats) throws Exception {
final int BATCH_SIZE = 50_000;
- // The reader thread produces batches of nodes
BlockingQueue> nodeBatchQueue = new LinkedBlockingQueue<>(200);
int numProcessors = Math.max(1, config.getImportConfiguration().getThreads());
CountDownLatch latch = new CountDownLatch(numProcessors);
try (ExecutorService executor = createExecutorService(numProcessors)) {
- // The reader logic is good, no changes needed here.
Thread reader = Thread.ofVirtual().start(() -> {
List buf = new ArrayList<>(BATCH_SIZE);
try {
@@ -684,11 +717,8 @@ private void cacheNeededNodeCoordinates(Path pbfFile, RocksDB neededNodesDb, Roc
for (int i = 0; i < numProcessors; i++) {
executor.submit(() -> {
stats.incrementActiveThreads();
- // --- OPTIMIZATION 1: ThreadLocal for reusable objects to reduce GC pressure ---
final ThreadLocal nodeWriterLocal = ThreadLocal.withInitial(() -> new RocksBatchWriter(nodeCache, 50_000, stats));
- // Reuse a ByteBuffer for writing values
final ThreadLocal valueBufferLocal = ThreadLocal.withInitial(() -> ByteBuffer.allocate(16));
- // Reuse a List of byte[] for multiGet keys. We only need one per thread.
final ThreadLocal> keysListLocal = ThreadLocal.withInitial(ArrayList::new);
try {
@@ -697,9 +727,8 @@ private void cacheNeededNodeCoordinates(Path pbfFile, RocksDB neededNodesDb, Roc
stats.setQueueSize(nodeBatchQueue.size());
if (nodes.isEmpty()) break;
- // Reuse the keys list
List keys = keysListLocal.get();
- keys.clear(); // Clear previous batch's keys
+ keys.clear();
for (OsmNode n : nodes) {
keys.add(s2Helper.longToByteArray(n.getId()));
}
@@ -707,19 +736,14 @@ private void cacheNeededNodeCoordinates(Path pbfFile, RocksDB neededNodesDb, Roc
List presence = neededNodesDb.multiGetAsList(keys);
RocksBatchWriter nodeWriter = nodeWriterLocal.get();
- ByteBuffer valueBuffer = valueBufferLocal.get(); // Get the reusable buffer
+ ByteBuffer valueBuffer = valueBufferLocal.get();
- // --- OPTIMIZATION 2: Combine loops ---
for (int idx = 0; idx < nodes.size(); idx++) {
if (presence.get(idx) != null) {
OsmNode n = nodes.get(idx);
-
- // Reset buffer position and write new data
valueBuffer.clear();
valueBuffer.putDouble(n.getLatitude());
valueBuffer.putDouble(n.getLongitude());
-
- // The key is already in the 'keys' list at the same index
nodeWriter.put(keys.get(idx), valueBuffer.array());
stats.incrementNodesCached();
}
@@ -737,8 +761,8 @@ private void cacheNeededNodeCoordinates(Path pbfFile, RocksDB neededNodesDb, Roc
stats.recordError(ImportStatistics.Stage.CACHING_NODE_COORDINATES, Kind.STORE, null, "node-cache-writer-close", e);
}
nodeWriterLocal.remove();
- valueBufferLocal.remove(); // Clean up thread-local
- keysListLocal.remove(); // Clean up thread-local
+ valueBufferLocal.remove();
+ keysListLocal.remove();
stats.decrementActiveThreads();
latch.countDown();
}
@@ -787,16 +811,15 @@ private void processAdministrativeBoundariesFromIndex(RocksDB relIndexDb, RocksD
org.locationtech.jts.geom.Geometry simplified = geometrySimplificationService.simplifyByAdminLevel(geometry, rec.level);
- // Simplification can produce invalid results too
if (simplified == null || simplified.isEmpty() || !simplified.isValid()) {
try {
simplified = simplified != null ? simplified.buffer(0) : null;
} catch (Exception e) {
- simplified = geometry; // fall back to unsimplified
+ simplified = geometry;
}
}
if (simplified == null || simplified.isEmpty() || !simplified.isValid()) {
- simplified = geometry; // fall back to unsimplified
+ simplified = geometry;
}
return new BoundaryResultLite(rec.osmId, rec.level, rec.name, rec.code, simplified);
@@ -805,7 +828,7 @@ private void processAdministrativeBoundariesFromIndex(RocksDB relIndexDb, RocksD
stats.decrementActiveThreads();
}
});
- ;
+
submitted.incrementAndGet();
for (Future f; (f = ecs.poll()) != null; ) {
@@ -844,6 +867,164 @@ private void processAdministrativeBoundariesFromIndex(RocksDB relIndexDb, RocksD
}
}
+ private void processBuildingBoundariesFromIndex(RocksDB poiIndexDb, RocksDB nodeCache, RocksDB wayIndexDb, RocksDB buildingGridIndexDb, RocksDB appendBuildingDb, ImportStatistics stats) throws Exception {
+ int maxInFlight = 100;
+ Semaphore semaphore = new Semaphore(maxInFlight);
+ int batchSize = 1000;
+ int numThreads = Math.max(1, config.getImportConfiguration().getThreads());
+ ExecutorService executor = createExecutorService(numThreads);
+ ExecutorCompletionService> ecs = new ExecutorCompletionService<>(executor);
+
+ AtomicInteger submitted = new AtomicInteger(0);
+ AtomicLong collected = new AtomicLong(0);
+
+ AtomicReference