This is an automated email from the ASF dual-hosted git repository.

bchapuis pushed a commit to branch flatgeobuf
in repository https://gitbox.apache.org/repos/asf/incubator-baremaps.git

commit 1f71d24e5c16a4a65bf532d5873ac28a8f09806d
Author: Bertil Chapuis <[email protected]>
AuthorDate: Mon Jun 17 13:36:37 2024 +0200

    Add support for flatgeobuf
---
 baremaps-core/pom.xml                              |   4 +
 baremaps-flatgeobuf/pom.xml                        |  29 ++
 .../baremaps/flatgeobuf/BoundedInputStream.java    |  74 ++++
 .../org/apache/baremaps/flatgeobuf/BufferUtil.java | 133 +++++++
 .../org/apache/baremaps/flatgeobuf/Constants.java  |  37 ++
 .../baremaps/flatgeobuf/FeatureMetaIterator.java   |  85 ++++
 .../org/apache/baremaps/flatgeobuf/FlatGeoBuf.java | 231 +++++++++++
 .../baremaps/flatgeobuf/FlatGeoBufReader.java      | 131 ++++++
 .../baremaps/flatgeobuf/FlatGeoBufWriter.java      | 160 ++++++++
 .../baremaps/flatgeobuf/GeometryConversions.java   | 353 ++++++++++++++++
 .../baremaps/flatgeobuf/GeometryOffsets.java       |  30 ++
 .../org/apache/baremaps/flatgeobuf/NodeItem.java   |  82 ++++
 .../apache/baremaps/flatgeobuf/PackedRTree.java    | 443 +++++++++++++++++++++
 .../baremaps/flatgeobuf/generated/Column.java      | 233 +++++++++++
 .../baremaps/flatgeobuf/generated/ColumnType.java  |  46 +++
 .../apache/baremaps/flatgeobuf/generated/Crs.java  | 185 +++++++++
 .../baremaps/flatgeobuf/generated/Feature.java     | 188 +++++++++
 .../baremaps/flatgeobuf/generated/Geometry.java    | 397 ++++++++++++++++++
 .../flatgeobuf/generated/GeometryType.java         |  51 +++
 .../baremaps/flatgeobuf/generated/Header.java      | 340 ++++++++++++++++
 .../src/main/resources/fbs/feature.fbs             |  22 +
 .../src/main/resources/fbs/header.fbs              |  84 ++++
 .../apache/baremaps/flatgeobuf/BufferUtilTest.java | 127 ++++++
 .../apache/baremaps/flatgeobuf/FlatGeoBufTest.java |  76 ++++
 baremaps-testing/pom.xml                           |   6 +
 pom.xml                                            |   5 +-
 scripts/generate-flatgeobuf.sh                     |   6 +
 27 files changed, 3554 insertions(+), 4 deletions(-)

diff --git a/baremaps-core/pom.xml b/baremaps-core/pom.xml
index 28fc6af3..b5c39796 100644
--- a/baremaps-core/pom.xml
+++ b/baremaps-core/pom.xml
@@ -146,6 +146,10 @@ limitations under the License.
       <groupId>org.wololo</groupId>
       <artifactId>flatgeobuf</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.wololo</groupId>
+      <artifactId>flatgeobuf</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.xerial</groupId>
       <artifactId>sqlite-jdbc</artifactId>
diff --git a/baremaps-flatgeobuf/pom.xml b/baremaps-flatgeobuf/pom.xml
new file mode 100644
index 00000000..e1955007
--- /dev/null
+++ b/baremaps-flatgeobuf/pom.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/maven-v4_0_0.xsd";>
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.apache.baremaps</groupId>
+    <artifactId>baremaps</artifactId>
+    <version>0.7.4-SNAPSHOT</version>
+  </parent>
+  <artifactId>baremaps-flatgeobuf</artifactId>
+  <dependencies>
+    <dependency>
+      <groupId>com.google.flatbuffers</groupId>
+      <artifactId>flatbuffers-java</artifactId>
+      <version>24.3.25</version>
+    </dependency>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.baremaps</groupId>
+      <artifactId>baremaps-testing</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.locationtech.jts</groupId>
+      <artifactId>jts-core</artifactId>
+    </dependency>
+  </dependencies>
+</project>
diff --git 
a/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/BoundedInputStream.java
 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/BoundedInputStream.java
new file mode 100644
index 00000000..38b91bf5
--- /dev/null
+++ 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/BoundedInputStream.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.baremaps.flatgeobuf;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class BoundedInputStream extends InputStream {
+  private final InputStream in;
+  private long remaining;
+
+  public BoundedInputStream(InputStream in, long size) {
+    this.in = in;
+    this.remaining = size;
+  }
+
+  @Override
+  public int read() throws IOException {
+    if (remaining == 0) {
+      return -1;
+    }
+    int result = in.read();
+    if (result != -1) {
+      remaining--;
+    }
+    return result;
+  }
+
+  @Override
+  public int read(byte[] b, int off, int len) throws IOException {
+    if (remaining == 0) {
+      return -1;
+    }
+    int toRead = (int) Math.min(len, remaining);
+    int result = in.read(b, off, toRead);
+    if (result != -1) {
+      remaining -= result;
+    }
+    return result;
+  }
+
+  @Override
+  public long skip(long n) throws IOException {
+    long toSkip = Math.min(n, remaining);
+    long skipped = in.skip(toSkip);
+    remaining -= skipped;
+    return skipped;
+  }
+
+  @Override
+  public int available() throws IOException {
+    return (int) Math.min(in.available(), remaining);
+  }
+
+  @Override
+  public void close() throws IOException {
+    in.close();
+  }
+}
diff --git 
a/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/BufferUtil.java
 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/BufferUtil.java
new file mode 100644
index 00000000..5530a340
--- /dev/null
+++ 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/BufferUtil.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.baremaps.flatgeobuf;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.ReadableByteChannel;
+
+public class BufferUtil {
+
+  private BufferUtil() {
+    // Prevent instantiation
+  }
+
+  public static ByteBuffer createByteBuffer(int capacity, ByteOrder order) {
+    ByteBuffer buffer = ByteBuffer.allocate(capacity).order(order);
+    buffer.flip();
+    return buffer;
+  }
+
+  /**
+   * Skips the given number of bytes from the specified channel, accounting 
for the bytes already in
+   * the buffer.
+   *
+   * @param channel the channel to skip bytes from
+   * @param buffer the buffer to use
+   * @param bytesToSkip the number of bytes to skip
+   * @return the buffer after skipping the specified number of bytes
+   * @throws IOException if an I/O error occurs while reading from the channel
+   */
+  public static ByteBuffer skipBytes(ReadableByteChannel channel, ByteBuffer 
buffer,
+      long bytesToSkip) throws IOException {
+    if (channel == null || buffer == null) {
+      throw new IllegalArgumentException("Channel and buffer must not be 
null");
+    }
+
+    if (bytesToSkip < 0) {
+      throw new IllegalArgumentException("The number of bytes to skip must be 
non-negative");
+    }
+
+    // If the buffer already has `bytesToSkip` or more bytes remaining, simply 
adjust the position.
+    if (buffer.remaining() >= bytesToSkip) {
+      buffer.position(buffer.position() + (int) bytesToSkip);
+      return buffer;
+    }
+
+    // Calculate the number of bytes we still need to skip after accounting 
for the buffer's
+    // remaining bytes.
+    long remainingBytesToSkip = bytesToSkip - buffer.remaining();
+
+    // Clear the buffer to prepare it for reading.
+    buffer.clear();
+
+    // Skip bytes directly from the channel.
+    while (remainingBytesToSkip > 0) {
+      // Read into the buffer to discard the data.
+      int bytesRead = channel.read(buffer);
+      if (bytesRead == -1) {
+        break; // End of channel reached
+      }
+      remainingBytesToSkip -= bytesRead;
+      buffer.clear();
+    }
+
+    return buffer;
+  }
+
+  /**
+   * Prepares the given buffer for reading at least `n` bytes from the 
specified channel.
+   *
+   * @param channel the channel to read bytes from
+   * @param buffer the buffer to prepare for reading
+   * @param bytesToRead the minimum number of bytes the buffer should contain
+   * @return a ByteBuffer that contains at least `n` bytes read from the 
channel
+   * @throws IOException if an I/O error occurs while reading from the channel
+   */
+  public static ByteBuffer readBytes(ReadableByteChannel channel, ByteBuffer 
buffer,
+      int bytesToRead) throws IOException {
+    if (channel == null || buffer == null) {
+      throw new IllegalArgumentException("Channel and buffer must not be 
null");
+    }
+
+    if (bytesToRead < 0) {
+      throw new IllegalArgumentException("The number of bytes to read must be 
non-negative");
+    }
+
+    // If the buffer already has `n` or more bytes remaining, it will be 
returned as is.
+    if (buffer.remaining() >= bytesToRead) {
+      return buffer;
+    }
+
+    // If the buffer has sufficient capacity but fewer than `n` bytes 
remaining, compact it and read
+    // more bytes.
+    if (buffer.capacity() >= bytesToRead) {
+      buffer.compact();
+      while (buffer.position() < bytesToRead) {
+        if (channel.read(buffer) == -1) {
+          break; // End of channel reached
+        }
+      }
+      buffer.flip();
+      return buffer;
+    }
+
+    // If the buffer has insufficient capacity, allocate a new buffer with the 
required capacity.
+    ByteBuffer newBuffer = 
ByteBuffer.allocate(bytesToRead).order(buffer.order());
+    buffer.flip();
+    newBuffer.put(buffer);
+    while (newBuffer.position() < bytesToRead) {
+      if (channel.read(newBuffer) == -1) {
+        break; // End of channel reached
+      }
+    }
+    newBuffer.flip();
+    return newBuffer;
+  }
+}
diff --git 
a/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/Constants.java
 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/Constants.java
new file mode 100644
index 00000000..1a15e1b7
--- /dev/null
+++ 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/Constants.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.baremaps.flatgeobuf;
+
+import java.nio.ByteBuffer;
+
+public class Constants {
+
+  public static final byte[] MAGIC_BYTES =
+      new byte[] {0x66, 0x67, 0x62, 0x03, 0x66, 0x67, 0x62, 0x00};
+
+  public static boolean isFlatgeobuf(ByteBuffer bb) {
+    return bb.get() == MAGIC_BYTES[0] &&
+        bb.get() == MAGIC_BYTES[1] &&
+        bb.get() == MAGIC_BYTES[2] &&
+        bb.get() == MAGIC_BYTES[3] &&
+        bb.get() == MAGIC_BYTES[4] &&
+        bb.get() == MAGIC_BYTES[5] &&
+        bb.get() == MAGIC_BYTES[6] &&
+        bb.get() == MAGIC_BYTES[7];
+  }
+}
diff --git 
a/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/FeatureMetaIterator.java
 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/FeatureMetaIterator.java
new file mode 100644
index 00000000..c11b93f6
--- /dev/null
+++ 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/FeatureMetaIterator.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.baremaps.flatgeobuf;
+
+//
+// import java.io.IOException;
+// import java.nio.ByteBuffer;
+// import java.nio.channels.ReadableByteChannel;
+// import java.nio.channels.SeekableByteChannel;
+// import java.util.Iterator;
+// import java.util.NoSuchElementException;
+//
+// public class FeatureMetaIterator implements Iterator<FeatureMeta> {
+//
+// private final HeaderMeta headerMeta;
+//
+// private final ReadableByteChannel channel;
+//
+// private final ByteBuffer buffer;
+//
+// private long cursor = 0;
+//
+// /**
+// * Constructs a row iterator.
+// *
+// * @param channel the channel to read from
+// * @param headerMeta the header meta
+// * @param buffer the buffer to use
+// */
+// public FeatureMetaIterator(
+// SeekableByteChannel channel,
+// HeaderMeta headerMeta,
+// ByteBuffer buffer) {
+// this.channel = channel;
+// this.headerMeta = headerMeta;
+// this.buffer = buffer;
+// }
+//
+// /**
+// * {@inheritDoc}
+// */
+// @Override
+// public boolean hasNext() {
+// return cursor < headerMeta.featuresCount;
+// }
+//
+// /**
+// * {@inheritDoc}
+// */
+// @Override
+// public FeatureMeta next() {
+// try {
+// channel.read(buffer);
+// buffer.flip();
+//
+// var featureSize = buffer.getInt();
+// var featureMeta = FlatGeoBufReader.readFeature(buffer, headerMeta);
+//
+// buffer.position(Integer.BYTES + featureSize);
+// buffer.compact();
+//
+// cursor++;
+//
+// return featureMeta;
+// } catch (IOException e) {
+// throw new NoSuchElementException(e);
+// }
+// }
+//
+// }
diff --git 
a/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/FlatGeoBuf.java
 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/FlatGeoBuf.java
new file mode 100644
index 00000000..0a07db14
--- /dev/null
+++ 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/FlatGeoBuf.java
@@ -0,0 +1,231 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.baremaps.flatgeobuf;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.IntStream;
+import org.apache.baremaps.flatgeobuf.FlatGeoBuf.Header.Feature;
+import org.locationtech.jts.geom.Geometry;
+
+public class FlatGeoBuf {
+
+  private FlatGeoBuf() {
+    // Prevent instantiation
+  }
+
+  // Geometry type enumeration
+  public enum GeometryType {
+    UNKNOWN(0),
+    POINT(1),
+    LINESTRING(2),
+    POLYGON(3),
+    MULTIPOINT(4),
+    MULTILINESTRING(5),
+    MULTIPOLYGON(6),
+    GEOMETRYCOLLECTION(7),
+    CIRCULARSTRING(8),
+    COMPOUNDCURVE(9),
+    CURVEPOLYGON(10),
+    MULTICURVE(11),
+    MULTISURFACE(12),
+    CURVE(13),
+    SURFACE(14),
+    POLYHEDRALSURFACE(15),
+    TIN(16),
+    TRIANGLE(17);
+
+    private final int value;
+
+    GeometryType(int value) {
+      this.value = value;
+    }
+
+    public int getValue() {
+      return value;
+    }
+  }
+
+  public enum ColumnType {
+    BYTE,
+    UBYTE,
+    BOOL,
+    SHORT,
+    USHORT,
+    INT,
+    UINT,
+    LONG,
+    ULONG,
+    FLOAT,
+    DOUBLE,
+    STRING,
+    JSON,
+    DATETIME,
+    BINARY
+  }
+
+  public record Column(
+      String name,
+      ColumnType type,
+      String title,
+      String description,
+      int width,
+      int precision,
+      int scale,
+      boolean nullable,
+      boolean unique,
+      boolean primaryKey,
+      String metadata) {
+  }
+
+  public record Crs(
+      String org,
+      int code,
+      String name,
+      String description,
+      String wkt,
+      String codeString) {
+  }
+
+  public record Header(
+      String name,
+      double[] envelope,
+      GeometryType geometryType,
+      boolean hasZ,
+      boolean hasM,
+      boolean hasT,
+      boolean hasTM,
+      List<Column> columns,
+      long featuresCount,
+      int indexNodeSize,
+      Crs crs,
+      String title,
+      String description,
+      String metadata) {
+    public Header {
+      indexNodeSize = indexNodeSize == 0 ? 16 : indexNodeSize;
+    }
+
+    public record Feature(
+        Geometry geometry,
+        List<Object> properties) {
+    }
+  }
+
+  public static Header 
asHeaderRecord(org.apache.baremaps.flatgeobuf.generated.Header header) {
+    return new Header(
+        header.name(),
+        new double[] {
+            header.envelope(0),
+            header.envelope(1),
+            header.envelope(2),
+            header.envelope(3)
+        },
+        GeometryType.values()[header.geometryType()],
+        header.hasZ(),
+        header.hasM(),
+        header.hasT(),
+        header.hasTm(),
+        IntStream.range(0, header.columnsLength())
+            .mapToObj(header::columns)
+            .map(column -> new Column(
+                column.name(),
+                ColumnType.values()[column.type()],
+                column.title(),
+                column.description(),
+                column.width(),
+                column.precision(),
+                column.scale(),
+                column.nullable(),
+                column.unique(),
+                column.primaryKey(),
+                column.metadata()))
+            .toList(),
+        header.featuresCount(),
+        header.indexNodeSize(),
+        new Crs(
+            header.crs().org(),
+            header.crs().code(),
+            header.crs().name(),
+            header.crs().description(),
+            header.crs().wkt(),
+            header.crs().codeString()),
+        header.title(),
+        header.description(),
+        header.metadata());
+  }
+
+  public static Feature 
asFeatureRecord(org.apache.baremaps.flatgeobuf.generated.Header header,
+      org.apache.baremaps.flatgeobuf.generated.Feature feature) {
+    var values = new ArrayList<>();
+    if (feature.propertiesLength() > 0) {
+      var propertiesBuffer = feature.propertiesAsByteBuffer();
+      while (propertiesBuffer.hasRemaining()) {
+        var columnPosition = propertiesBuffer.getShort();
+        var columnType = header.columns(columnPosition);
+        var columnValue = readValue(propertiesBuffer, columnType);
+        values.add(columnValue);
+      }
+    }
+    return new Feature(
+        GeometryConversions.readGeometry(feature.geometry(), 
header.geometryType()),
+        values);
+  }
+
+  private static Object readValue(ByteBuffer buffer,
+      org.apache.baremaps.flatgeobuf.generated.Column column) {
+    return switch (ColumnType.values()[column.type()]) {
+      case BYTE -> buffer.get();
+      case UBYTE -> buffer.get();
+      case BOOL -> buffer.get() == 1;
+      case SHORT -> buffer.getShort();
+      case USHORT -> buffer.getShort();
+      case INT -> buffer.getInt();
+      case UINT -> buffer.getInt();
+      case LONG -> buffer.getLong();
+      case ULONG -> buffer.getLong();
+      case FLOAT -> buffer.getFloat();
+      case DOUBLE -> buffer.getDouble();
+      case STRING -> readString(buffer);
+      case JSON -> readJson(buffer);
+      case DATETIME -> readDateTime(buffer);
+      case BINARY -> readBinary(buffer);
+    };
+  }
+
+  private static Object readString(ByteBuffer buffer) {
+    var length = buffer.getInt();
+    var bytes = new byte[length];
+    buffer.get(bytes);
+    return new String(bytes, StandardCharsets.UTF_8);
+  }
+
+  private static Object readJson(ByteBuffer buffer) {
+    throw new UnsupportedOperationException();
+  }
+
+  private static Object readDateTime(ByteBuffer buffer) {
+    throw new UnsupportedOperationException();
+  }
+
+  private static Object readBinary(ByteBuffer buffer) {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git 
a/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/FlatGeoBufReader.java
 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/FlatGeoBufReader.java
new file mode 100644
index 00000000..f59b9397
--- /dev/null
+++ 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/FlatGeoBufReader.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.baremaps.flatgeobuf;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import org.apache.baremaps.flatgeobuf.generated.Feature;
+import org.apache.baremaps.flatgeobuf.generated.Header;
+
+public class FlatGeoBufReader {
+
+  public static Header readHeader(ReadableByteChannel channel)
+      throws IOException {
+
+    // Check if the file is a flatgeobuf
+    ByteBuffer buffer = BufferUtil.createByteBuffer(12, 
ByteOrder.LITTLE_ENDIAN);
+    BufferUtil.readBytes(channel, buffer, 12);
+    if (!Constants.isFlatgeobuf(buffer)) {
+      throw new IOException("This is not a flatgeobuf!");
+    }
+
+    // Read the header size
+    int headerSize = buffer.getInt();
+    ByteBuffer headerBuffer = BufferUtil.createByteBuffer(headerSize, 
ByteOrder.LITTLE_ENDIAN);
+    BufferUtil.readBytes(channel, headerBuffer, headerSize);
+
+    return Header.getRootAsHeader(headerBuffer);
+  }
+
+  public static void skipIndex(ReadableByteChannel channel, Header header)
+      throws IOException {
+    readIndexAsBuffer(channel, header);
+  }
+
+  public static ByteBuffer readIndexAsBuffer(ReadableByteChannel channel, 
Header header)
+      throws IOException {
+    long indexSize = PackedRTree.calcSize(header.featuresCount(), 
header.indexNodeSize());
+
+    if (indexSize > 1L << 31) {
+      throw new IOException("Index size is greater than 2GB!");
+    }
+    ByteBuffer buffer = BufferUtil.createByteBuffer((int) indexSize, 
ByteOrder.LITTLE_ENDIAN);
+    BufferUtil.readBytes(channel, buffer, (int) indexSize);
+
+    return buffer;
+  }
+
+  public static InputStream readIndexAsStream(ReadableByteChannel channel, 
Header header) {
+    long indexSize = PackedRTree.calcSize(header.featuresCount(), 
header.indexNodeSize());
+    return new BoundedInputStream(Channels.newInputStream(channel), indexSize);
+  }
+
+  public static Feature readFeature(ReadableByteChannel channel, ByteBuffer 
buffer)
+      throws IOException {
+    ByteBuffer newBuffer = BufferUtil.readBytes(channel, buffer, 4);
+    int featureSize = newBuffer.getInt();
+    newBuffer = BufferUtil.readBytes(channel, buffer, featureSize);
+    Feature feature = Feature.getRootAsFeature(newBuffer);
+    buffer.position(buffer.position() + featureSize);
+    return feature;
+  }
+
+  // var geometryBuffer = feature.geometry();
+  // var geometry = GeometryConversions.readGeometry(geometryBuffer, 
geometryBuffer.type());
+  // var properties = new ArrayList<>();
+  // if (feature.propertiesLength() > 0) {
+  // var propertiesBuffer = feature.propertiesAsByteBuffer();
+  // while (propertiesBuffer.hasRemaining()) {
+  // var type = propertiesBuffer.getShort();
+  // var column = header.columns.get(type);
+  // var value = readColumnValue(propertiesBuffer, column);
+  // properties.add(value);
+  // }
+  // }
+  // }
+  //
+  // public static Object readColumnValue(ByteBuffer buffer, ColumnMeta 
column) {
+  // return switch (column.type()) {
+  // case ColumnType.Byte -> buffer.get();
+  // case ColumnType.Bool -> buffer.get() == 1;
+  // case ColumnType.Short -> buffer.getShort();
+  // case ColumnType.Int -> buffer.getInt();
+  // case ColumnType.Long -> buffer.getLong();
+  // case ColumnType.Float -> buffer.getFloat();
+  // case ColumnType.Double -> buffer.getDouble();
+  // case ColumnType.String -> readColumnString(buffer);
+  // case ColumnType.Json -> readColumnJson(buffer);
+  // case ColumnType.DateTime -> readColumnDateTime(buffer);
+  // case ColumnType.Binary -> readColumnBinary(buffer);
+  // default -> null;
+  // };
+  // }
+  //
+  // public static Object readColumnString(ByteBuffer buffer) {
+  // var length = buffer.getInt();
+  // var bytes = new byte[length];
+  // buffer.get(bytes);
+  // return new String(bytes, StandardCharsets.UTF_8);
+  // }
+  //
+  // public static Object readColumnJson(ByteBuffer buffer) {
+  // throw new UnsupportedOperationException();
+  // }
+  //
+  // public static Object readColumnDateTime(ByteBuffer buffer) {
+  // throw new UnsupportedOperationException();
+  // }
+  //
+  // public static Object readColumnBinary(ByteBuffer buffer) {
+  // throw new UnsupportedOperationException();
+  // }
+}
diff --git 
a/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/FlatGeoBufWriter.java
 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/FlatGeoBufWriter.java
new file mode 100644
index 00000000..c1872857
--- /dev/null
+++ 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/FlatGeoBufWriter.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.baremaps.flatgeobuf;
+
+//
+// import com.google.flatbuffers.FlatBufferBuilder;
+// import java.io.IOException;
+// import java.io.OutputStream;
+// import java.nio.ByteBuffer;
+// import java.nio.ByteOrder;
+// import java.nio.channels.Channels;
+// import java.nio.channels.WritableByteChannel;
+// import java.nio.charset.StandardCharsets;
+//
+// import org.wololo.flatgeobuf.generated.*;
+//
+// public class FlatGeoBufWriter {
+// public static void writeColumnValue(ByteBuffer buffer, ColumnMeta column, 
Object value) {
+// switch (column.type()) {
+// case ColumnType.Byte -> buffer.put((byte) value);
+// case ColumnType.Bool -> buffer.put((byte) ((boolean) value ? 1 : 0));
+// case ColumnType.Short -> buffer.putShort((short) value);
+// case ColumnType.Int -> buffer.putInt((int) value);
+// case ColumnType.Long -> buffer.putLong((long) value);
+// case ColumnType.Float -> buffer.putFloat((float) value);
+// case ColumnType.Double -> buffer.putDouble((double) value);
+// case ColumnType.String -> writeColumnString(buffer, value);
+// case ColumnType.Json -> writeColumnJson(buffer, value);
+// case ColumnType.DateTime -> writeColumnDateTime(buffer, value);
+// case ColumnType.Binary -> writeColumnBinary(buffer, value);
+// default -> {
+// // Do nothing
+// }
+// };
+// }
+//
+// public static void writeColumnString(ByteBuffer propertiesBuffer, Object 
value) {
+// var bytes = ((String) value).getBytes(StandardCharsets.UTF_8);
+// propertiesBuffer.putInt(bytes.length);
+// propertiesBuffer.put(bytes);
+// }
+//
+// public static void writeColumnJson(ByteBuffer propertiesBuffer, Object 
value) {
+// throw new UnsupportedOperationException();
+// }
+//
+// public static void writeColumnDateTime(ByteBuffer propertiesBuffer, Object 
value) {
+// throw new UnsupportedOperationException();
+// }
+//
+// public static void writeColumnBinary(ByteBuffer propertiesBuffer, Object 
value) {
+// throw new UnsupportedOperationException();
+// }
+//
+// public static void writeFeature(
+// OutputStream outputStream, HeaderMeta headerMeta,
+// FeatureMeta featureMeta) throws IOException {
+// var featureBuilder = new FlatBufferBuilder(4096);
+//
+// // Write the properties
+// var propertiesBuffer = ByteBuffer.allocate(1 << 
20).order(ByteOrder.LITTLE_ENDIAN);
+// var properties = featureMeta.properties();
+// for (int i = 0; i < properties.size(); i++) {
+// var column = headerMeta.columns.get(i);
+// var value = properties.get(i);
+// propertiesBuffer.putShort((short) i);
+// writeColumnValue(propertiesBuffer, column, value);
+// }
+// if (propertiesBuffer.position() > 0) {
+// propertiesBuffer.flip();
+// }
+// var propertiesOffset = Feature.createPropertiesVector(featureBuilder, 
propertiesBuffer);
+//
+// // Write the geometry
+// var geometry = featureMeta.geometry();
+// var geometryOffset = 0;
+// if (geometry != null) {
+// geometryOffset =
+// GeometryConversions.writeGeometry(featureBuilder, geometry, 
headerMeta.geometryType);
+// }
+//
+// // Write the feature
+// var featureOffset = Feature.createFeature(featureBuilder, geometryOffset, 
propertiesOffset, 0);
+// featureBuilder.finishSizePrefixed(featureOffset);
+//
+// byte[] data = featureBuilder.sizedByteArray();
+// outputStream.write(data);
+// }
+//
+// public static void write(HeaderMeta headerMeta, OutputStream to, 
FlatBufferBuilder builder)
+// throws IOException {
+// int[] columnsArray = headerMeta.columns.stream().mapToInt(c -> {
+// int nameOffset = builder.createString(c.name());
+// int type = c.type();
+// return Column.createColumn(
+// builder,
+// nameOffset,
+// type,
+// 0,
+// 0,
+// c.width(),
+// c.precision(),
+// c.scale(),
+// c.nullable(),
+// c.unique(),
+// c.primaryKey(),
+// 0);
+// }).toArray();
+// int columnsOffset = Header.createColumnsVector(builder, columnsArray);
+//
+// int nameOffset = 0;
+// if (headerMeta.name != null) {
+// nameOffset = builder.createString(headerMeta.name);
+// }
+// int crsOffset = 0;
+// if (headerMeta.srid != 0) {
+// Crs.startCrs(builder);
+// Crs.addCode(builder, headerMeta.srid);
+// crsOffset = Crs.endCrs(builder);
+// }
+// int envelopeOffset = 0;
+// if (headerMeta.envelope != null) {
+// envelopeOffset = Header.createEnvelopeVector(builder,
+// new double[] {headerMeta.envelope.getMinX(), headerMeta.envelope.getMinY(),
+// headerMeta.envelope.getMaxX(), headerMeta.envelope.getMaxY()});
+// }
+// Header.startHeader(builder);
+// Header.addGeometryType(builder, headerMeta.geometryType);
+// Header.addIndexNodeSize(builder, headerMeta.indexNodeSize);
+// Header.addColumns(builder, columnsOffset);
+// Header.addEnvelope(builder, envelopeOffset);
+// Header.addName(builder, nameOffset);
+// Header.addCrs(builder, crsOffset);
+// Header.addFeaturesCount(builder, headerMeta.featuresCount);
+// int offset = Header.endHeader(builder);
+//
+// builder.finishSizePrefixed(offset);
+//
+// WritableByteChannel channel = Channels.newChannel(to);
+// ByteBuffer dataBuffer = builder.dataBuffer();
+// while (dataBuffer.hasRemaining()) {
+// channel.write(dataBuffer);
+// }
+// }
+// }
diff --git 
a/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/GeometryConversions.java
 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/GeometryConversions.java
new file mode 100644
index 00000000..359b9e79
--- /dev/null
+++ 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/GeometryConversions.java
@@ -0,0 +1,353 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.baremaps.flatgeobuf;
+
+import com.google.flatbuffers.FlatBufferBuilder;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.function.IntFunction;
+import java.util.function.Supplier;
+import org.apache.baremaps.flatgeobuf.generated.Geometry;
+import org.apache.baremaps.flatgeobuf.generated.GeometryType;
+import org.locationtech.jts.geom.*;
+
+public class GeometryConversions {
+
+  public static GeometryOffsets writeGeometryPart(
+      FlatBufferBuilder builder,
+      org.locationtech.jts.geom.Geometry geometry,
+      int geometryType) throws IOException {
+
+    GeometryOffsets go = new GeometryOffsets();
+
+    if (geometry == null) {
+      return go;
+    }
+
+    if (geometryType == GeometryType.MultiLineString) {
+      int end = 0;
+      MultiLineString mls = (MultiLineString) geometry;
+      if (mls.getNumGeometries() > 1) {
+        go.ends = new long[mls.getNumGeometries()];
+        for (int i = 0; i < mls.getNumGeometries(); i++) {
+          go.ends[i] = end += mls.getGeometryN(i).getNumPoints();
+        }
+      }
+    } else if (geometryType == GeometryType.Polygon) {
+      Polygon p = (Polygon) geometry;
+      go.ends = new long[p.getNumInteriorRing() + 1];
+      int end = p.getExteriorRing().getNumPoints();
+      go.ends[0] = end;
+      for (int i = 0; i < p.getNumInteriorRing(); i++) {
+        go.ends[i + 1] = end += p.getInteriorRingN(i).getNumPoints();
+      }
+    } else if (geometryType == GeometryType.MultiPolygon) {
+      MultiPolygon mp = (MultiPolygon) geometry;
+      int numGeometries = mp.getNumGeometries();
+      GeometryOffsets[] gos = new GeometryOffsets[numGeometries];
+      for (int i = 0; i < numGeometries; i++) {
+        Polygon p = (Polygon) mp.getGeometryN(i);
+        gos[i] = writeGeometryPart(builder, p, GeometryType.Polygon);
+      }
+      go.gos = gos;
+      return go;
+    }
+
+    final int numPoints = geometry.getNumPoints();
+    // build the vector "manually", using a CoordinateSequenceFilter to avoid 
creating
+    // Coordinate arrays or any Coordinate at all, depending on the underlying
+    // CoordinateSequence implementation. Vector elements ought to be added in 
reverse order
+    Geometry.startXyVector(builder, 2 * numPoints);
+    ReverseXYCoordinateSequenceFilter filter = new 
ReverseXYCoordinateSequenceFilter(builder);
+    applyInReverseOrder(geometry, filter);
+    go.coordsOffset = builder.endVector();
+
+    if (filter.hasZ) {
+      Geometry.startZVector(builder, numPoints);
+      applyInReverseOrder(geometry, new 
OrdinateCoordinateSequenceFilter(builder, 2));
+      go.zOffset = builder.endVector();
+    } else {
+      go.zOffset = 0;
+    }
+
+    if (filter.hasM) {
+      Geometry.startMVector(builder, numPoints);
+      applyInReverseOrder(geometry, new 
OrdinateCoordinateSequenceFilter(builder, 3));
+      go.mOffset = builder.endVector();
+    } else {
+      go.mOffset = 0;
+    }
+
+    if (go.ends != null) {
+      go.endsOffset = Geometry.createEndsVector(builder, go.ends);
+    }
+
+    go.type = geometryType;
+
+    return go;
+  }
+
+  public static int writeGeometry(FlatBufferBuilder builder,
+      org.locationtech.jts.geom.Geometry geometry, byte geometryType) throws 
IOException {
+    byte knownGeometryType = geometryType;
+    if (geometryType == GeometryType.Unknown) {
+      knownGeometryType = 
GeometryConversions.toGeometryType(geometry.getClass());
+    }
+
+    GeometryOffsets go =
+        GeometryConversions.writeGeometryPart(builder, geometry, 
knownGeometryType);
+
+    int geometryOffset;
+    if (go.gos != null && go.gos.length > 0) {
+      int[] partOffsets = new int[go.gos.length];
+      for (int i = 0; i < go.gos.length; i++) {
+        GeometryOffsets goPart = go.gos[i];
+        int partOffset = Geometry.createGeometry(builder, goPart.endsOffset,
+            goPart.coordsOffset, goPart.zOffset, goPart.mOffset, 0, 0, 
goPart.type, 0);
+        partOffsets[i] = partOffset;
+      }
+      int partsOffset = Geometry.createPartsVector(builder, partOffsets);
+      geometryOffset = Geometry.createGeometry(builder, 0, 0, 0, 0, 0, 0,
+          geometryType == GeometryType.Unknown ? knownGeometryType : 0, 
partsOffset);
+    } else {
+      geometryOffset =
+          Geometry.createGeometry(builder, go.endsOffset, go.coordsOffset, 
go.zOffset, go.mOffset,
+              0, 0, geometryType == GeometryType.Unknown ? knownGeometryType : 
0, 0);
+    }
+    return geometryOffset;
+  }
+
+  /**
+   * Applies the {@code filter} to all {@link 
org.locationtech.jts.geom.Geometry#getGeometryN(int)
+   * subgeometries} in reverse order if it's a {@link GeometryCollection} or a 
{@link Polygon} (i.e.
+   * interior rings in reverse order first)
+   */
+  private static void applyInReverseOrder(org.locationtech.jts.geom.Geometry 
geometry,
+      CoordinateSequenceFilter filter) {
+
+    final int numGeometries = geometry.getNumGeometries();
+    if (numGeometries > 1) {
+      for (int i = numGeometries - 1; i >= 0; i--) {
+        org.locationtech.jts.geom.Geometry sub = geometry.getGeometryN(i);
+        applyInReverseOrder(sub, filter);
+      }
+    } else if (geometry instanceof Polygon) {
+      Polygon p = (Polygon) geometry;
+      for (int i = p.getNumInteriorRing() - 1; i >= 0; i--) {
+        org.locationtech.jts.geom.Geometry hole = p.getInteriorRingN(i);
+        applyInReverseOrder(hole, filter);
+      }
+      applyInReverseOrder(p.getExteriorRing(), filter);
+    } else {
+      geometry.apply(filter);
+    }
+  }
+
+
+  private static class OrdinateCoordinateSequenceFilter implements 
CoordinateSequenceFilter {
+    private FlatBufferBuilder builder;
+    private final int ordinateIndex;
+
+    OrdinateCoordinateSequenceFilter(FlatBufferBuilder builder, int 
ordinateIndex) {
+      this.builder = builder;
+      this.ordinateIndex = ordinateIndex;
+    }
+
+    public @Override void filter(final CoordinateSequence seq, final int 
coordIndex) {
+      int reverseSeqIndex = seq.size() - coordIndex - 1;
+      builder.addDouble(seq.getOrdinate(reverseSeqIndex, ordinateIndex));
+    }
+
+    public @Override boolean isGeometryChanged() {
+      return false;
+    }
+
+    public @Override boolean isDone() {
+      return false;
+    }
+  }
+
+  private static class ReverseXYCoordinateSequenceFilter implements 
CoordinateSequenceFilter {
+    private FlatBufferBuilder builder;
+    boolean hasZ = false;
+    boolean hasM = false;
+
+    ReverseXYCoordinateSequenceFilter(FlatBufferBuilder builder) {
+      this.builder = builder;
+    }
+
+    public @Override void filter(final CoordinateSequence seq, final int 
coordIndex) {
+      int reverseSeqIndex = seq.size() - coordIndex - 1;
+      double y = seq.getOrdinate(reverseSeqIndex, 1);
+      double x = seq.getOrdinate(reverseSeqIndex, 0);
+      builder.addDouble(y);
+      builder.addDouble(x);
+      if (!hasZ && seq.hasZ()) {
+        hasZ = true;
+      }
+      if (!hasM && seq.hasM()) {
+        hasM = true;
+      }
+    }
+
+    public boolean isHasZ() {
+      return hasZ;
+    }
+
+    public boolean isHasM() {
+      return hasM;
+    }
+
+    public @Override boolean isGeometryChanged() {
+      return false;
+    }
+
+    public @Override boolean isDone() {
+      return false;
+    }
+  }
+
+  public static org.locationtech.jts.geom.Geometry readGeometry(Geometry 
geometry,
+      int geometryType) {
+    GeometryFactory factory = new GeometryFactory();
+
+    if (geometryType == GeometryType.MultiPolygon) {
+      int partsLength = geometry.partsLength();
+      Polygon[] polygons = new Polygon[partsLength];
+      for (int i = 0; i < geometry.partsLength(); i++) {
+        polygons[i] = (Polygon) readGeometry(geometry.parts(i), 
GeometryType.Polygon);
+      }
+      return factory.createMultiPolygon(polygons);
+    }
+
+    int xyLength = geometry.xyLength();
+
+    Coordinate[] coordinates = new Coordinate[xyLength >> 1];
+
+    int c = 0;
+    for (int i = 0; i < xyLength; i = i + 2) {
+      if (c < geometry.mLength()) {
+        coordinates[c++] = new CoordinateXYZM(geometry.xy(i), geometry.xy(i + 
1),
+            (i >> 1) < geometry.zLength() ? geometry.z((i >> 1)) : 
Coordinate.NULL_ORDINATE,
+            (i >> 1) < geometry.mLength() ? geometry.m((i >> 1)) : 
Coordinate.NULL_ORDINATE);
+      } else {
+        coordinates[c++] = new Coordinate(geometry.xy(i), geometry.xy(i + 1),
+            (i >> 1) < geometry.zLength() ? geometry.z((i >> 1)) : 
Coordinate.NULL_ORDINATE);
+      }
+    }
+
+    IntFunction<Polygon> makePolygonWithRings = (int endsLength) -> {
+      LinearRing[] lrs = new LinearRing[endsLength];
+      int s = 0;
+      for (int i = 0; i < endsLength; i++) {
+        int e = (int) geometry.ends(i);
+        Coordinate[] cs = Arrays.copyOfRange(coordinates, s, e);
+        lrs[i] = factory.createLinearRing(cs);
+        s = e;
+      }
+      LinearRing shell = lrs[0];
+      LinearRing holes[] = Arrays.copyOfRange(lrs, 1, endsLength);
+      return factory.createPolygon(shell, holes);
+    };
+
+    Supplier<Polygon> makePolygon = () -> {
+      int endsLength = geometry.endsLength();
+      if (endsLength > 1) {
+        return makePolygonWithRings.apply(endsLength);
+      } else {
+        return factory.createPolygon(coordinates);
+      }
+    };
+
+    switch (geometryType) {
+      case GeometryType.Unknown:
+        return null;
+      case GeometryType.Point:
+        if (coordinates.length > 0) {
+          return factory.createPoint(coordinates[0]);
+        } else {
+          return factory.createPoint();
+        }
+      case GeometryType.MultiPoint:
+        return factory.createMultiPointFromCoords(coordinates);
+      case GeometryType.LineString:
+        return factory.createLineString(coordinates);
+      case GeometryType.MultiLineString: {
+        int lengthLengths = geometry.endsLength();
+        if (lengthLengths < 2) {
+          return factory
+              .createMultiLineString(new LineString[] 
{factory.createLineString(coordinates)});
+        }
+        LineString[] lss = new LineString[lengthLengths];
+        int s = 0;
+        for (int i = 0; i < lengthLengths; i++) {
+          int e = (int) geometry.ends(i);
+          Coordinate[] cs = Arrays.copyOfRange(coordinates, s, e);
+          lss[i] = factory.createLineString(cs);
+          s = e;
+        }
+        return factory.createMultiLineString(lss);
+      }
+      case GeometryType.Polygon:
+        return makePolygon.get();
+      default:
+        throw new RuntimeException("Unknown geometry type");
+    }
+  }
+
+  public static Class<?> getGeometryClass(int geometryType) {
+    switch (geometryType) {
+      case GeometryType.Unknown:
+        return Geometry.class;
+      case GeometryType.Point:
+        return Point.class;
+      case GeometryType.MultiPoint:
+        return MultiPoint.class;
+      case GeometryType.LineString:
+        return LineString.class;
+      case GeometryType.MultiLineString:
+        return MultiLineString.class;
+      case GeometryType.Polygon:
+        return Polygon.class;
+      case GeometryType.MultiPolygon:
+        return MultiPolygon.class;
+      default:
+        throw new RuntimeException("Unknown geometry type");
+    }
+  }
+
+  public static byte toGeometryType(Class<?> geometryClass) {
+    if (geometryClass == org.locationtech.jts.geom.Geometry.class) {
+      return GeometryType.Unknown;
+    } else if (MultiPoint.class.isAssignableFrom(geometryClass)) {
+      return GeometryType.MultiPoint;
+    } else if (Point.class.isAssignableFrom(geometryClass)) {
+      return GeometryType.Point;
+    } else if (MultiLineString.class.isAssignableFrom(geometryClass)) {
+      return GeometryType.MultiLineString;
+    } else if (LineString.class.isAssignableFrom(geometryClass)) {
+      return GeometryType.LineString;
+    } else if (MultiPolygon.class.isAssignableFrom(geometryClass)) {
+      return GeometryType.MultiPolygon;
+    } else if (Polygon.class.isAssignableFrom(geometryClass)) {
+      return GeometryType.Polygon;
+    } else {
+      throw new RuntimeException("Unknown geometry type");
+    }
+  }
+}
diff --git 
a/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/GeometryOffsets.java
 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/GeometryOffsets.java
new file mode 100644
index 00000000..c4c22a43
--- /dev/null
+++ 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/GeometryOffsets.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.baremaps.flatgeobuf;
+
+public class GeometryOffsets {
+  public int coordsOffset;
+  public int zOffset;
+  public int mOffset;
+  public long[] ends = null;
+  public int[] lengths = null;
+  public int endsOffset = 0;
+  public int lengthsOffset = 0;
+  public int type = 0;
+  public GeometryOffsets[] gos = null;
+}
diff --git 
a/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/NodeItem.java
 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/NodeItem.java
new file mode 100644
index 00000000..c55a5c51
--- /dev/null
+++ 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/NodeItem.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.baremaps.flatgeobuf;
+
+import org.locationtech.jts.geom.Envelope;
+
+public record NodeItem(
+    double minX,
+    double minY,
+    double maxX,
+    double maxY,
+    long offset) {
+
+  public NodeItem(double minX, double minY, double maxX, double maxY) {
+    this(minX, minY, maxX, maxY, 0);
+  }
+
+  public NodeItem(long offset) {
+    this(
+        Double.POSITIVE_INFINITY,
+        Double.POSITIVE_INFINITY,
+        Double.NEGATIVE_INFINITY,
+        Double.NEGATIVE_INFINITY,
+        offset);
+  }
+
+  public double width() {
+    return maxX - minX;
+  }
+
+  public double height() {
+    return maxY - minY;
+  }
+
+  public static NodeItem sum(NodeItem a, final NodeItem b) {
+    return a.expand(b);
+  }
+
+  public NodeItem expand(final NodeItem nodeItem) {
+    return new NodeItem(
+        Math.min(nodeItem.minX, minX),
+        Math.min(nodeItem.minY, minY),
+        Math.max(nodeItem.maxX, maxX),
+        Math.max(nodeItem.maxY, maxY),
+        offset);
+  }
+
+  public boolean intersects(NodeItem nodeItem) {
+    if (nodeItem.minX > maxX) {
+      return false;
+    }
+    if (nodeItem.minY > maxY) {
+      return false;
+    }
+    if (nodeItem.maxX < minX) {
+      return false;
+    }
+    if (nodeItem.maxY < minY) {
+      return false;
+    }
+    return true;
+  }
+
+  public Envelope toEnvelope() {
+    return new Envelope(minX, maxX, minY, maxY);
+  }
+}
diff --git 
a/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/PackedRTree.java
 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/PackedRTree.java
new file mode 100644
index 00000000..14f8fb56
--- /dev/null
+++ 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/PackedRTree.java
@@ -0,0 +1,443 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.baremaps.flatgeobuf;
+
+import com.google.common.io.LittleEndianDataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.*;
+import org.apache.baremaps.flatgeobuf.generated.Header;
+import org.locationtech.jts.geom.Envelope;
+
+public class PackedRTree {
+
+  private static int NODE_ITEM_LEN = 8 * 4 + 8;
+  final static int HILBERT_MAX = (1 << 16) - 1;
+  private int numItems;
+  private int nodeSize;
+  public NodeItem[] nodeItems;
+  private long numNodes;
+  private List<Pair<Integer, Integer>> levelBounds;
+
+  public PackedRTree(final List<? extends Item> items, final short nodeSize) {
+    this.numItems = items.size();
+    init(nodeSize);
+    int k = (int) (this.numNodes - (long) this.numItems);
+    Iterator<? extends Item> it = items.iterator();
+    for (int i = 0; i < this.numItems; ++i) {
+      this.nodeItems[k++] = it.next().nodeItem;
+    }
+    generateNodes();
+  }
+
+  public void init(int nodeSize) {
+    if (nodeSize < 2)
+      throw new RuntimeException("Node size must be at least 2");
+    if (numItems == 0)
+      throw new RuntimeException("Number of items must be greater than 0");
+    this.nodeSize = Math.min(Math.max(2, nodeSize), HILBERT_MAX);
+    this.levelBounds = generateLevelBounds(numItems, this.nodeSize);
+    this.numNodes = levelBounds.get(0).second;
+    this.nodeItems = new NodeItem[Math.toIntExact(numNodes)];
+  }
+
+  void generateNodes() {
+    long pos;
+    long end = 0;
+    for (short i = 0; i < levelBounds.size() - 1; i++) {
+      pos = levelBounds.get(i).first;
+      end = levelBounds.get(i).second;
+      long newpos = levelBounds.get(i + 1).first;
+      while (pos < end) {
+        NodeItem node = new NodeItem(pos);
+        for (short j = 0; j < this.nodeSize && pos < end; j++)
+          node.expand(nodeItems[(int) pos++]);
+        nodeItems[(int) newpos++] = node;
+      }
+    }
+  }
+
+  public static List<? extends Item> hilbertSort(List<? extends Item> items, 
NodeItem extent) {
+    double minX = extent.minX();
+    double minY = extent.minY();
+    double width = extent.width();
+    double height = extent.height();
+    Collections.sort(items, (a, b) -> {
+      long ha = hibert(a.nodeItem, HILBERT_MAX, minX, minY, width, height);
+      long hb = hibert(b.nodeItem, HILBERT_MAX, minX, minY, width, height);
+      return (ha - hb) > 0 ? 1 : (ha - hb) == 0 ? 0 : -1;
+    });
+    return items;
+  }
+
+  public static long hibert(NodeItem nodeItem, int hilbertMax, double minX, 
double minY,
+      double width, double height) {
+    long x = 0;
+    long y = 0;
+    if (width != 0.0)
+      x = (long) Math.floor(hilbertMax * ((nodeItem.minX() + nodeItem.maxX()) 
/ 2 - minX) / width);
+    if (height != 0.0)
+      y = (long) Math.floor(hilbertMax * ((nodeItem.minY() + nodeItem.maxY()) 
/ 2 - minY) / height);
+    return hibert(x, y);
+  }
+
+  // Based on public domain code at 
https://github.com/rawrunprotected/hilbert_curves
+  private static long hibert(long x, long y) {
+    long a = x ^ y;
+    long b = 0xFFFF ^ a;
+    long c = 0xFFFF ^ (x | y);
+    long d = x & (y ^ 0xFFFF);
+    long A = a | (b >> 1);
+    long B = (a >> 1) ^ a;
+    long C = ((c >> 1) ^ (b & (d >> 1))) ^ c;
+    long D = ((a & (c >> 1)) ^ (d >> 1)) ^ d;
+
+    a = A;
+    b = B;
+    c = C;
+    d = D;
+    A = ((a & (a >> 2)) ^ (b & (b >> 2)));
+    B = ((a & (b >> 2)) ^ (b & ((a ^ b) >> 2)));
+    C ^= ((a & (c >> 2)) ^ (b & (d >> 2)));
+    D ^= ((b & (c >> 2)) ^ ((a ^ b) & (d >> 2)));
+
+    a = A;
+    b = B;
+    c = C;
+    d = D;
+    A = ((a & (a >> 4)) ^ (b & (b >> 4)));
+    B = ((a & (b >> 4)) ^ (b & ((a ^ b) >> 4)));
+    C ^= ((a & (c >> 4)) ^ (b & (d >> 4)));
+    D ^= ((b & (c >> 4)) ^ ((a ^ b) & (d >> 4)));
+
+    a = A;
+    b = B;
+    c = C;
+    d = D;
+    C ^= ((a & (c >> 8)) ^ (b & (d >> 8)));
+    D ^= ((b & (c >> 8)) ^ ((a ^ b) & (d >> 8)));
+
+    a = C ^ (C >> 1);
+    b = D ^ (D >> 1);
+
+    long i0 = x ^ y;
+    long i1 = b | (0xFFFF ^ (i0 | a));
+
+    i0 = (i0 | (i0 << 8)) & 0x00FF00FF;
+    i0 = (i0 | (i0 << 4)) & 0x0F0F0F0F;
+    i0 = (i0 | (i0 << 2)) & 0x33333333;
+    i0 = (i0 | (i0 << 1)) & 0x55555555;
+
+    i1 = (i1 | (i1 << 8)) & 0x00FF00FF;
+    i1 = (i1 | (i1 << 4)) & 0x0F0F0F0F;
+    i1 = (i1 | (i1 << 2)) & 0x33333333;
+    i1 = (i1 | (i1 << 1)) & 0x55555555;
+
+    long value = ((i1 << 1) | i0);
+
+    return value;
+  }
+
+  public static NodeItem calcExtent(List<? extends Item> items) {
+    return items.stream().map(item -> item.nodeItem).reduce(new NodeItem(0),
+        (nodeItem, nodeItem2) -> nodeItem.expand(nodeItem2));
+  }
+
+  public void write(OutputStream outputStream) {
+    // nodeItem 40 Byte
+    ByteBuffer buffer = ByteBuffer.allocate((int) (NODE_ITEM_LEN * numNodes));
+    buffer.order(ByteOrder.LITTLE_ENDIAN);
+    for (NodeItem nodeItem : nodeItems) {
+      buffer.putDouble(nodeItem.minX());
+      buffer.putDouble(nodeItem.minY());
+      buffer.putDouble(nodeItem.maxX());
+      buffer.putDouble(nodeItem.maxY());
+      buffer.putLong(nodeItem.offset());
+    }
+    buffer.flip();
+    try {
+      if (buffer.hasRemaining()) {
+        byte[] arr = new byte[buffer.remaining()];
+        buffer.get(arr);
+        outputStream.write(arr);
+        outputStream.flush();
+      }
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    } finally {
+      buffer.clear();
+      buffer = null;
+    }
+  }
+
+  public static long calcSize(long numItems, int nodeSize) {
+    if (nodeSize < 2)
+      throw new RuntimeException("Node size must be at least 2");
+    if (numItems == 0)
+      throw new RuntimeException("Number of items must be greater than 0");
+    int nodeSizeMin = Math.min(Math.max(nodeSize, 2), 65535);
+    // limit so that resulting size in bytes can be represented by ulong
+    if (numItems > 1 << 56)
+      throw new IndexOutOfBoundsException("Number of items must be less than 
2^56");
+    long n = numItems;
+    long numNodes = n;
+    do {
+      n = (n + nodeSizeMin - 1) / nodeSizeMin;
+      numNodes += n;
+    } while (n != 1);
+    return numNodes * NODE_ITEM_LEN;
+  }
+
+  static List<Pair<Integer, Integer>> generateLevelBounds(int numItems, int 
nodeSize) {
+    if (nodeSize < 2)
+      throw new RuntimeException("Node size must be at least 2");
+    if (numItems == 0)
+      throw new RuntimeException("Number of items must be greater than 0");
+
+    // number of nodes per level in bottom-up order
+    int n = numItems;
+    int numNodes = n;
+    ArrayList<Integer> levelNumNodes = new ArrayList<Integer>();
+    levelNumNodes.add(n);
+    do {
+      n = (n + nodeSize - 1) / nodeSize;
+      numNodes += n;
+      levelNumNodes.add(n);
+    } while (n != 1);
+
+    // offsets per level in reversed storage order (top-down)
+    ArrayList<Integer> levelOffsets = new ArrayList<Integer>();
+    n = numNodes;
+    for (int size : levelNumNodes)
+      levelOffsets.add(n -= size);
+    List<Pair<Integer, Integer>> levelBounds = new LinkedList<>();
+    // bounds per level in reversed storage order (top-down)
+    for (int i = 0; i < levelNumNodes.size(); i++)
+      levelBounds.add(new Pair<>(levelOffsets.get(i), levelOffsets.get(i) + 
levelNumNodes.get(i)));
+    return levelBounds;
+  }
+
+  private static class QueueItem {
+    public QueueItem(long nodeIndex, int level) {
+      this.nodeIndex = nodeIndex;
+      this.level = level;
+    }
+
+    long nodeIndex;
+    int level;
+  }
+
+  public static class SearchHit {
+    public SearchHit(long offset, long index) {
+      this.offset = offset;
+      this.index = index;
+    }
+
+    public long offset;
+    public long index;
+  }
+
+  public static ArrayList<SearchHit> search(ByteBuffer bb, int start, int 
numItems, int nodeSize,
+      Envelope rect) {
+    ArrayList<SearchHit> searchHits = new ArrayList<SearchHit>();
+    double minX = rect.getMinX();
+    double minY = rect.getMinY();
+    double maxX = rect.getMaxX();
+    double maxY = rect.getMaxY();
+    List<Pair<Integer, Integer>> levelBounds = generateLevelBounds(numItems, 
nodeSize);
+    int leafNodesOffset = levelBounds.get(0).first;
+    int numNodes = levelBounds.get(0).second;
+    Deque<QueueItem> queue = new LinkedList<QueueItem>();
+    queue.add(new QueueItem(0, levelBounds.size() - 1));
+    while (queue.size() != 0) {
+      QueueItem stackItem = queue.pop();
+      int nodeIndex = (int) stackItem.nodeIndex;
+      int level = stackItem.level;
+      boolean isLeafNode = nodeIndex >= numNodes - numItems;
+      // find the end index of the node
+      int levelEnd = levelBounds.get(level).second;
+      int end = Math.min(nodeIndex + nodeSize, levelEnd);
+      int nodeStart = start + (nodeIndex * NODE_ITEM_LEN);
+      // int length = end - nodeIndex;
+      // search through child nodes
+      for (int pos = nodeIndex; pos < end; pos++) {
+        int offset = nodeStart + ((pos - nodeIndex) * NODE_ITEM_LEN);
+        double nodeMinX = bb.getDouble(offset + 0);
+        double nodeMinY = bb.getDouble(offset + 8);
+        double nodeMaxX = bb.getDouble(offset + 16);
+        double nodeMaxY = bb.getDouble(offset + 24);
+        if (maxX < nodeMinX)
+          continue;
+        if (maxY < nodeMinY)
+          continue;
+        if (minX > nodeMaxX)
+          continue;
+        if (minY > nodeMaxY)
+          continue;
+        long indexOffset = bb.getLong(offset + 32);
+        if (isLeafNode)
+          searchHits.add(new SearchHit(indexOffset, pos - leafNodesOffset));
+        else
+          queue.add(new QueueItem(indexOffset, level - 1));
+      }
+    }
+    return searchHits;
+  }
+
+  public static class SearchResult {
+    public ArrayList<SearchHit> hits = new ArrayList<SearchHit>();
+    public int pos;
+  }
+
+  public static SearchResult search(InputStream stream, int start, int 
numItems, int nodeSize,
+      Envelope rect) throws IOException {
+    LittleEndianDataInputStream data = new LittleEndianDataInputStream(stream);
+    int dataPos = 0;
+    int skip;
+    SearchResult searchResult = new SearchResult();
+    double minX = rect.getMinX();
+    double minY = rect.getMinY();
+    double maxX = rect.getMaxX();
+    double maxY = rect.getMaxY();
+    List<Pair<Integer, Integer>> levelBounds = generateLevelBounds(numItems, 
nodeSize);
+    int leafNodesOffset = levelBounds.get(0).first;
+    int numNodes = levelBounds.get(0).second;
+    Deque<QueueItem> queue = new LinkedList<QueueItem>();
+    queue.add(new QueueItem(0, levelBounds.size() - 1));
+    while (queue.size() != 0) {
+      QueueItem stackItem = queue.pop();
+      int nodeIndex = (int) stackItem.nodeIndex;
+      int level = stackItem.level;
+      boolean isLeafNode = nodeIndex >= numNodes - numItems;
+      // find the end index of the node
+      int levelBound = levelBounds.get(level).second;
+      int end = Math.min(nodeIndex + nodeSize, levelBound);
+      int nodeStart = nodeIndex * NODE_ITEM_LEN;
+      skip = nodeStart - dataPos;
+      if (skip > 0) {
+        skipNBytes(data, skip);
+        dataPos += skip;
+      }
+      // int length = end - nodeIndex;
+      // search through child nodes
+      for (int pos = nodeIndex; pos < end; pos++) {
+        int offset = nodeStart + ((pos - nodeIndex) * NODE_ITEM_LEN);
+        skip = offset - dataPos;
+        if (skip > 0) {
+          skipNBytes(data, skip);
+          dataPos += skip;
+        }
+        double nodeMinX = data.readDouble();
+        dataPos += 8;
+        if (maxX < nodeMinX)
+          continue;
+        double nodeMinY = data.readDouble();
+        dataPos += 8;
+        if (maxY < nodeMinY)
+          continue;
+        double nodeMaxX = data.readDouble();
+        dataPos += 8;
+        if (minX > nodeMaxX)
+          continue;
+        double nodeMaxY = data.readDouble();
+        dataPos += 8;
+        if (minY > nodeMaxY)
+          continue;
+        long indexOffset = data.readLong();
+        dataPos += 8;
+        if (isLeafNode)
+          searchResult.hits.add(new SearchHit(indexOffset, pos - 
leafNodesOffset));
+        else
+          queue.add(new QueueItem(indexOffset, level - 1));
+      }
+    }
+    searchResult.pos = dataPos;
+    return searchResult;
+  }
+
+  public static long[] readFeatureOffsets(
+      LittleEndianDataInputStream data, long[] fids, Header header)
+      throws IOException {
+
+    long treeSize = calcSize((int) header.featuresCount(), 
header.indexNodeSize());
+    List<Pair<Integer, Integer>> levelBounds =
+        generateLevelBounds((int) header.featuresCount(), 
header.indexNodeSize());
+    long bottomLevelOffset = levelBounds.get(0).first * 40;
+
+    long pos = 0;
+    long[] featureOffsets = new long[fids.length];
+    for (int i = 0; i < fids.length; i++) {
+      if (fids[i] > header.featuresCount() - 1)
+        throw new NoSuchElementException();
+      long nodeItemOffset = bottomLevelOffset + (fids[i] * 40);
+      long delta = nodeItemOffset + (8 * 4) - pos;
+      skipNBytes(data, delta);
+      long featureOffset = data.readLong();
+      pos += delta + 8;
+      featureOffsets[i] = featureOffset;
+    }
+    long remainingIndexOffset = treeSize - pos;
+    skipNBytes(data, remainingIndexOffset);
+
+    return featureOffsets;
+  }
+
+  static void skipNBytes(InputStream stream, long skip) throws IOException {
+    long remaining = skip;
+    while (remaining > 0) {
+      remaining -= stream.skip(remaining);
+    }
+  }
+
+  public static class Item {
+    public NodeItem nodeItem;
+  }
+
+  public static class FeatureItem extends Item {
+    public long size;
+    public long offset;
+  }
+
+  static class Pair<T, U> {
+    public T first;
+    public U second;
+
+    public Pair(T first, U second) {
+      this.first = first;
+      this.second = second;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o)
+        return true;
+      if (o == null || getClass() != o.getClass())
+        return false;
+      Pair<?, ?> pair = (Pair<?, ?>) o;
+      return Objects.equals(first, pair.first) && Objects.equals(second, 
pair.second);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(first, second);
+    }
+  }
+}
diff --git 
a/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/generated/Column.java
 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/generated/Column.java
new file mode 100644
index 00000000..02aaf9e2
--- /dev/null
+++ 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/generated/Column.java
@@ -0,0 +1,233 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.baremaps.flatgeobuf.generated;
+
+import com.google.flatbuffers.BaseVector;
+import com.google.flatbuffers.Constants;
+import com.google.flatbuffers.FlatBufferBuilder;
+import com.google.flatbuffers.Table;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@SuppressWarnings("unused")
+public final class Column extends Table {
+  public static void ValidateVersion() {
+    Constants.FLATBUFFERS_24_3_25();
+  }
+
+  public static Column getRootAsColumn(ByteBuffer _bb) {
+    return getRootAsColumn(_bb, new Column());
+  }
+
+  public static Column getRootAsColumn(ByteBuffer _bb, Column 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 Column __assign(int _i, ByteBuffer _bb) {
+    __init(_i, _bb);
+    return this;
+  }
+
+  public String name() {
+    int o = __offset(4);
+    return o != 0 ? __string(o + bb_pos) : null;
+  }
+
+  public ByteBuffer nameAsByteBuffer() {
+    return __vector_as_bytebuffer(4, 1);
+  }
+
+  public ByteBuffer nameInByteBuffer(ByteBuffer _bb) {
+    return __vector_in_bytebuffer(_bb, 4, 1);
+  }
+
+  public int type() {
+    int o = __offset(6);
+    return o != 0 ? bb.get(o + bb_pos) & 0xFF : 0;
+  }
+
+  public String title() {
+    int o = __offset(8);
+    return o != 0 ? __string(o + bb_pos) : null;
+  }
+
+  public ByteBuffer titleAsByteBuffer() {
+    return __vector_as_bytebuffer(8, 1);
+  }
+
+  public ByteBuffer titleInByteBuffer(ByteBuffer _bb) {
+    return __vector_in_bytebuffer(_bb, 8, 1);
+  }
+
+  public String description() {
+    int o = __offset(10);
+    return o != 0 ? __string(o + bb_pos) : null;
+  }
+
+  public ByteBuffer descriptionAsByteBuffer() {
+    return __vector_as_bytebuffer(10, 1);
+  }
+
+  public ByteBuffer descriptionInByteBuffer(ByteBuffer _bb) {
+    return __vector_in_bytebuffer(_bb, 10, 1);
+  }
+
+  public int width() {
+    int o = __offset(12);
+    return o != 0 ? bb.getInt(o + bb_pos) : -1;
+  }
+
+  public int precision() {
+    int o = __offset(14);
+    return o != 0 ? bb.getInt(o + bb_pos) : -1;
+  }
+
+  public int scale() {
+    int o = __offset(16);
+    return o != 0 ? bb.getInt(o + bb_pos) : -1;
+  }
+
+  public boolean nullable() {
+    int o = __offset(18);
+    return o != 0 ? 0 != bb.get(o + bb_pos) : true;
+  }
+
+  public boolean unique() {
+    int o = __offset(20);
+    return o != 0 ? 0 != bb.get(o + bb_pos) : false;
+  }
+
+  public boolean primaryKey() {
+    int o = __offset(22);
+    return o != 0 ? 0 != bb.get(o + bb_pos) : false;
+  }
+
+  public String metadata() {
+    int o = __offset(24);
+    return o != 0 ? __string(o + bb_pos) : null;
+  }
+
+  public ByteBuffer metadataAsByteBuffer() {
+    return __vector_as_bytebuffer(24, 1);
+  }
+
+  public ByteBuffer metadataInByteBuffer(ByteBuffer _bb) {
+    return __vector_in_bytebuffer(_bb, 24, 1);
+  }
+
+  public static int createColumn(FlatBufferBuilder builder,
+      int nameOffset,
+      int type,
+      int titleOffset,
+      int descriptionOffset,
+      int width,
+      int precision,
+      int scale,
+      boolean nullable,
+      boolean unique,
+      boolean primaryKey,
+      int metadataOffset) {
+    builder.startTable(11);
+    Column.addMetadata(builder, metadataOffset);
+    Column.addScale(builder, scale);
+    Column.addPrecision(builder, precision);
+    Column.addWidth(builder, width);
+    Column.addDescription(builder, descriptionOffset);
+    Column.addTitle(builder, titleOffset);
+    Column.addName(builder, nameOffset);
+    Column.addPrimaryKey(builder, primaryKey);
+    Column.addUnique(builder, unique);
+    Column.addNullable(builder, nullable);
+    Column.addType(builder, type);
+    return Column.endColumn(builder);
+  }
+
+  public static void startColumn(FlatBufferBuilder builder) {
+    builder.startTable(11);
+  }
+
+  public static void addName(FlatBufferBuilder builder, int nameOffset) {
+    builder.addOffset(0, nameOffset, 0);
+  }
+
+  public static void addType(FlatBufferBuilder builder, int type) {
+    builder.addByte(1, (byte) type, (byte) 0);
+  }
+
+  public static void addTitle(FlatBufferBuilder builder, int titleOffset) {
+    builder.addOffset(2, titleOffset, 0);
+  }
+
+  public static void addDescription(FlatBufferBuilder builder, int 
descriptionOffset) {
+    builder.addOffset(3, descriptionOffset, 0);
+  }
+
+  public static void addWidth(FlatBufferBuilder builder, int width) {
+    builder.addInt(4, width, -1);
+  }
+
+  public static void addPrecision(FlatBufferBuilder builder, int precision) {
+    builder.addInt(5, precision, -1);
+  }
+
+  public static void addScale(FlatBufferBuilder builder, int scale) {
+    builder.addInt(6, scale, -1);
+  }
+
+  public static void addNullable(FlatBufferBuilder builder, boolean nullable) {
+    builder.addBoolean(7, nullable, true);
+  }
+
+  public static void addUnique(FlatBufferBuilder builder, boolean unique) {
+    builder.addBoolean(8, unique, false);
+  }
+
+  public static void addPrimaryKey(FlatBufferBuilder builder, boolean 
primaryKey) {
+    builder.addBoolean(9, primaryKey, false);
+  }
+
+  public static void addMetadata(FlatBufferBuilder builder, int 
metadataOffset) {
+    builder.addOffset(10, metadataOffset, 0);
+  }
+
+  public static int endColumn(FlatBufferBuilder builder) {
+    int o = builder.endTable();
+    builder.required(o, 4); // name
+    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 Column get(int j) {
+      return get(new Column(), j);
+    }
+
+    public Column get(Column obj, int j) {
+      return obj.__assign(__indirect(__element(j), bb), bb);
+    }
+  }
+}
diff --git 
a/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/generated/ColumnType.java
 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/generated/ColumnType.java
new file mode 100644
index 00000000..1da21f8e
--- /dev/null
+++ 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/generated/ColumnType.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.baremaps.flatgeobuf.generated;
+
+@SuppressWarnings("unused")
+public final class ColumnType {
+  private ColumnType() {}
+
+  public static final int Byte = 0;
+  public static final int UByte = 1;
+  public static final int Bool = 2;
+  public static final int Short = 3;
+  public static final int UShort = 4;
+  public static final int Int = 5;
+  public static final int UInt = 6;
+  public static final int Long = 7;
+  public static final int ULong = 8;
+  public static final int Float = 9;
+  public static final int Double = 10;
+  public static final int String = 11;
+  public static final int Json = 12;
+  public static final int DateTime = 13;
+  public static final int Binary = 14;
+
+  public static final String[] names = {"Byte", "UByte", "Bool", "Short", 
"UShort", "Int", "UInt",
+      "Long", "ULong", "Float", "Double", "String", "Json", "DateTime", 
"Binary",};
+
+  public static String name(int e) {
+    return names[e];
+  }
+}
diff --git 
a/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/generated/Crs.java
 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/generated/Crs.java
new file mode 100644
index 00000000..291de1dc
--- /dev/null
+++ 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/generated/Crs.java
@@ -0,0 +1,185 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.baremaps.flatgeobuf.generated;
+
+import com.google.flatbuffers.BaseVector;
+import com.google.flatbuffers.Constants;
+import com.google.flatbuffers.FlatBufferBuilder;
+import com.google.flatbuffers.Table;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@SuppressWarnings("unused")
+public final class Crs extends Table {
+  public static void ValidateVersion() {
+    Constants.FLATBUFFERS_24_3_25();
+  }
+
+  public static Crs getRootAsCrs(ByteBuffer _bb) {
+    return getRootAsCrs(_bb, new Crs());
+  }
+
+  public static Crs getRootAsCrs(ByteBuffer _bb, Crs 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 Crs __assign(int _i, ByteBuffer _bb) {
+    __init(_i, _bb);
+    return this;
+  }
+
+  public String org() {
+    int o = __offset(4);
+    return o != 0 ? __string(o + bb_pos) : null;
+  }
+
+  public ByteBuffer orgAsByteBuffer() {
+    return __vector_as_bytebuffer(4, 1);
+  }
+
+  public ByteBuffer orgInByteBuffer(ByteBuffer _bb) {
+    return __vector_in_bytebuffer(_bb, 4, 1);
+  }
+
+  public int code() {
+    int o = __offset(6);
+    return o != 0 ? bb.getInt(o + bb_pos) : 0;
+  }
+
+  public String name() {
+    int o = __offset(8);
+    return o != 0 ? __string(o + bb_pos) : null;
+  }
+
+  public ByteBuffer nameAsByteBuffer() {
+    return __vector_as_bytebuffer(8, 1);
+  }
+
+  public ByteBuffer nameInByteBuffer(ByteBuffer _bb) {
+    return __vector_in_bytebuffer(_bb, 8, 1);
+  }
+
+  public String description() {
+    int o = __offset(10);
+    return o != 0 ? __string(o + bb_pos) : null;
+  }
+
+  public ByteBuffer descriptionAsByteBuffer() {
+    return __vector_as_bytebuffer(10, 1);
+  }
+
+  public ByteBuffer descriptionInByteBuffer(ByteBuffer _bb) {
+    return __vector_in_bytebuffer(_bb, 10, 1);
+  }
+
+  public String wkt() {
+    int o = __offset(12);
+    return o != 0 ? __string(o + bb_pos) : null;
+  }
+
+  public ByteBuffer wktAsByteBuffer() {
+    return __vector_as_bytebuffer(12, 1);
+  }
+
+  public ByteBuffer wktInByteBuffer(ByteBuffer _bb) {
+    return __vector_in_bytebuffer(_bb, 12, 1);
+  }
+
+  public String codeString() {
+    int o = __offset(14);
+    return o != 0 ? __string(o + bb_pos) : null;
+  }
+
+  public ByteBuffer codeStringAsByteBuffer() {
+    return __vector_as_bytebuffer(14, 1);
+  }
+
+  public ByteBuffer codeStringInByteBuffer(ByteBuffer _bb) {
+    return __vector_in_bytebuffer(_bb, 14, 1);
+  }
+
+  public static int createCrs(FlatBufferBuilder builder,
+      int orgOffset,
+      int code,
+      int nameOffset,
+      int descriptionOffset,
+      int wktOffset,
+      int codeStringOffset) {
+    builder.startTable(6);
+    Crs.addCodeString(builder, codeStringOffset);
+    Crs.addWkt(builder, wktOffset);
+    Crs.addDescription(builder, descriptionOffset);
+    Crs.addName(builder, nameOffset);
+    Crs.addCode(builder, code);
+    Crs.addOrg(builder, orgOffset);
+    return Crs.endCrs(builder);
+  }
+
+  public static void startCrs(FlatBufferBuilder builder) {
+    builder.startTable(6);
+  }
+
+  public static void addOrg(FlatBufferBuilder builder, int orgOffset) {
+    builder.addOffset(0, orgOffset, 0);
+  }
+
+  public static void addCode(FlatBufferBuilder builder, int code) {
+    builder.addInt(1, code, 0);
+  }
+
+  public static void addName(FlatBufferBuilder builder, int nameOffset) {
+    builder.addOffset(2, nameOffset, 0);
+  }
+
+  public static void addDescription(FlatBufferBuilder builder, int 
descriptionOffset) {
+    builder.addOffset(3, descriptionOffset, 0);
+  }
+
+  public static void addWkt(FlatBufferBuilder builder, int wktOffset) {
+    builder.addOffset(4, wktOffset, 0);
+  }
+
+  public static void addCodeString(FlatBufferBuilder builder, int 
codeStringOffset) {
+    builder.addOffset(5, codeStringOffset, 0);
+  }
+
+  public static int endCrs(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 Crs get(int j) {
+      return get(new Crs(), j);
+    }
+
+    public Crs get(Crs obj, int j) {
+      return obj.__assign(__indirect(__element(j), bb), bb);
+    }
+  }
+}
diff --git 
a/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/generated/Feature.java
 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/generated/Feature.java
new file mode 100644
index 00000000..0a9ba8f2
--- /dev/null
+++ 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/generated/Feature.java
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.baremaps.flatgeobuf.generated;
+
+import com.google.flatbuffers.BaseVector;
+import com.google.flatbuffers.ByteVector;
+import com.google.flatbuffers.Constants;
+import com.google.flatbuffers.FlatBufferBuilder;
+import com.google.flatbuffers.Table;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@SuppressWarnings("unused")
+public final class Feature extends Table {
+  public static void ValidateVersion() {
+    Constants.FLATBUFFERS_24_3_25();
+  }
+
+  public static Feature getRootAsFeature(ByteBuffer _bb) {
+    return getRootAsFeature(_bb, new Feature());
+  }
+
+  public static Feature getRootAsFeature(ByteBuffer _bb, Feature 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 Feature __assign(int _i, ByteBuffer _bb) {
+    __init(_i, _bb);
+    return this;
+  }
+
+  public Geometry geometry() {
+    return geometry(new Geometry());
+  }
+
+  public Geometry geometry(Geometry obj) {
+    int o = __offset(4);
+    return o != 0 ? obj.__assign(__indirect(o + bb_pos), bb) : null;
+  }
+
+  public int properties(int j) {
+    int o = __offset(6);
+    return o != 0 ? bb.get(__vector(o) + j * 1) & 0xFF : 0;
+  }
+
+  public int propertiesLength() {
+    int o = __offset(6);
+    return o != 0 ? __vector_len(o) : 0;
+  }
+
+  public ByteVector propertiesVector() {
+    return propertiesVector(new ByteVector());
+  }
+
+  public ByteVector propertiesVector(ByteVector obj) {
+    int o = __offset(6);
+    return o != 0 ? obj.__assign(__vector(o), bb) : null;
+  }
+
+  public ByteBuffer propertiesAsByteBuffer() {
+    return __vector_as_bytebuffer(6, 1);
+  }
+
+  public ByteBuffer propertiesInByteBuffer(ByteBuffer _bb) {
+    return __vector_in_bytebuffer(_bb, 6, 1);
+  }
+
+  public Column columns(int j) {
+    return columns(new Column(), j);
+  }
+
+  public Column columns(Column obj, int j) {
+    int o = __offset(8);
+    return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null;
+  }
+
+  public int columnsLength() {
+    int o = __offset(8);
+    return o != 0 ? __vector_len(o) : 0;
+  }
+
+  public Column.Vector columnsVector() {
+    return columnsVector(new Column.Vector());
+  }
+
+  public Column.Vector columnsVector(Column.Vector obj) {
+    int o = __offset(8);
+    return o != 0 ? obj.__assign(__vector(o), 4, bb) : null;
+  }
+
+  public static int createFeature(FlatBufferBuilder builder,
+      int geometryOffset,
+      int propertiesOffset,
+      int columnsOffset) {
+    builder.startTable(3);
+    Feature.addColumns(builder, columnsOffset);
+    Feature.addProperties(builder, propertiesOffset);
+    Feature.addGeometry(builder, geometryOffset);
+    return Feature.endFeature(builder);
+  }
+
+  public static void startFeature(FlatBufferBuilder builder) {
+    builder.startTable(3);
+  }
+
+  public static void addGeometry(FlatBufferBuilder builder, int 
geometryOffset) {
+    builder.addOffset(0, geometryOffset, 0);
+  }
+
+  public static void addProperties(FlatBufferBuilder builder, int 
propertiesOffset) {
+    builder.addOffset(1, propertiesOffset, 0);
+  }
+
+  public static int createPropertiesVector(FlatBufferBuilder builder, byte[] 
data) {
+    return builder.createByteVector(data);
+  }
+
+  public static int createPropertiesVector(FlatBufferBuilder builder, 
ByteBuffer data) {
+    return builder.createByteVector(data);
+  }
+
+  public static void startPropertiesVector(FlatBufferBuilder builder, int 
numElems) {
+    builder.startVector(1, numElems, 1);
+  }
+
+  public static void addColumns(FlatBufferBuilder builder, int columnsOffset) {
+    builder.addOffset(2, columnsOffset, 0);
+  }
+
+  public static int createColumnsVector(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 startColumnsVector(FlatBufferBuilder builder, int 
numElems) {
+    builder.startVector(4, numElems, 4);
+  }
+
+  public static int endFeature(FlatBufferBuilder builder) {
+    int o = builder.endTable();
+    return o;
+  }
+
+  public static void finishFeatureBuffer(FlatBufferBuilder builder, int 
offset) {
+    builder.finish(offset);
+  }
+
+  public static void finishSizePrefixedFeatureBuffer(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 Feature get(int j) {
+      return get(new Feature(), j);
+    }
+
+    public Feature get(Feature obj, int j) {
+      return obj.__assign(__indirect(__element(j), bb), bb);
+    }
+  }
+}
diff --git 
a/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/generated/Geometry.java
 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/generated/Geometry.java
new file mode 100644
index 00000000..02d93f42
--- /dev/null
+++ 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/generated/Geometry.java
@@ -0,0 +1,397 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.baremaps.flatgeobuf.generated;
+
+import com.google.flatbuffers.BaseVector;
+import com.google.flatbuffers.Constants;
+import com.google.flatbuffers.DoubleVector;
+import com.google.flatbuffers.FlatBufferBuilder;
+import com.google.flatbuffers.IntVector;
+import com.google.flatbuffers.LongVector;
+import com.google.flatbuffers.Table;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@SuppressWarnings("unused")
+public final class Geometry extends Table {
+  public static void ValidateVersion() {
+    Constants.FLATBUFFERS_24_3_25();
+  }
+
+  public static Geometry getRootAsGeometry(ByteBuffer _bb) {
+    return getRootAsGeometry(_bb, new Geometry());
+  }
+
+  public static Geometry getRootAsGeometry(ByteBuffer _bb, Geometry 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 Geometry __assign(int _i, ByteBuffer _bb) {
+    __init(_i, _bb);
+    return this;
+  }
+
+  public long ends(int j) {
+    int o = __offset(4);
+    return o != 0 ? (long) bb.getInt(__vector(o) + j * 4) & 0xFFFFFFFFL : 0;
+  }
+
+  public int endsLength() {
+    int o = __offset(4);
+    return o != 0 ? __vector_len(o) : 0;
+  }
+
+  public IntVector endsVector() {
+    return endsVector(new IntVector());
+  }
+
+  public IntVector endsVector(IntVector obj) {
+    int o = __offset(4);
+    return o != 0 ? obj.__assign(__vector(o), bb) : null;
+  }
+
+  public ByteBuffer endsAsByteBuffer() {
+    return __vector_as_bytebuffer(4, 4);
+  }
+
+  public ByteBuffer endsInByteBuffer(ByteBuffer _bb) {
+    return __vector_in_bytebuffer(_bb, 4, 4);
+  }
+
+  public double xy(int j) {
+    int o = __offset(6);
+    return o != 0 ? bb.getDouble(__vector(o) + j * 8) : 0;
+  }
+
+  public int xyLength() {
+    int o = __offset(6);
+    return o != 0 ? __vector_len(o) : 0;
+  }
+
+  public DoubleVector xyVector() {
+    return xyVector(new DoubleVector());
+  }
+
+  public DoubleVector xyVector(DoubleVector obj) {
+    int o = __offset(6);
+    return o != 0 ? obj.__assign(__vector(o), bb) : null;
+  }
+
+  public ByteBuffer xyAsByteBuffer() {
+    return __vector_as_bytebuffer(6, 8);
+  }
+
+  public ByteBuffer xyInByteBuffer(ByteBuffer _bb) {
+    return __vector_in_bytebuffer(_bb, 6, 8);
+  }
+
+  public double z(int j) {
+    int o = __offset(8);
+    return o != 0 ? bb.getDouble(__vector(o) + j * 8) : 0;
+  }
+
+  public int zLength() {
+    int o = __offset(8);
+    return o != 0 ? __vector_len(o) : 0;
+  }
+
+  public DoubleVector zVector() {
+    return zVector(new DoubleVector());
+  }
+
+  public DoubleVector zVector(DoubleVector obj) {
+    int o = __offset(8);
+    return o != 0 ? obj.__assign(__vector(o), bb) : null;
+  }
+
+  public ByteBuffer zAsByteBuffer() {
+    return __vector_as_bytebuffer(8, 8);
+  }
+
+  public ByteBuffer zInByteBuffer(ByteBuffer _bb) {
+    return __vector_in_bytebuffer(_bb, 8, 8);
+  }
+
+  public double m(int j) {
+    int o = __offset(10);
+    return o != 0 ? bb.getDouble(__vector(o) + j * 8) : 0;
+  }
+
+  public int mLength() {
+    int o = __offset(10);
+    return o != 0 ? __vector_len(o) : 0;
+  }
+
+  public DoubleVector mVector() {
+    return mVector(new DoubleVector());
+  }
+
+  public DoubleVector mVector(DoubleVector obj) {
+    int o = __offset(10);
+    return o != 0 ? obj.__assign(__vector(o), bb) : null;
+  }
+
+  public ByteBuffer mAsByteBuffer() {
+    return __vector_as_bytebuffer(10, 8);
+  }
+
+  public ByteBuffer mInByteBuffer(ByteBuffer _bb) {
+    return __vector_in_bytebuffer(_bb, 10, 8);
+  }
+
+  public double t(int j) {
+    int o = __offset(12);
+    return o != 0 ? bb.getDouble(__vector(o) + j * 8) : 0;
+  }
+
+  public int tLength() {
+    int o = __offset(12);
+    return o != 0 ? __vector_len(o) : 0;
+  }
+
+  public DoubleVector tVector() {
+    return tVector(new DoubleVector());
+  }
+
+  public DoubleVector tVector(DoubleVector obj) {
+    int o = __offset(12);
+    return o != 0 ? obj.__assign(__vector(o), bb) : null;
+  }
+
+  public ByteBuffer tAsByteBuffer() {
+    return __vector_as_bytebuffer(12, 8);
+  }
+
+  public ByteBuffer tInByteBuffer(ByteBuffer _bb) {
+    return __vector_in_bytebuffer(_bb, 12, 8);
+  }
+
+  public long tm(int j) {
+    int o = __offset(14);
+    return o != 0 ? bb.getLong(__vector(o) + j * 8) : 0;
+  }
+
+  public int tmLength() {
+    int o = __offset(14);
+    return o != 0 ? __vector_len(o) : 0;
+  }
+
+  public LongVector tmVector() {
+    return tmVector(new LongVector());
+  }
+
+  public LongVector tmVector(LongVector obj) {
+    int o = __offset(14);
+    return o != 0 ? obj.__assign(__vector(o), bb) : null;
+  }
+
+  public ByteBuffer tmAsByteBuffer() {
+    return __vector_as_bytebuffer(14, 8);
+  }
+
+  public ByteBuffer tmInByteBuffer(ByteBuffer _bb) {
+    return __vector_in_bytebuffer(_bb, 14, 8);
+  }
+
+  public int type() {
+    int o = __offset(16);
+    return o != 0 ? bb.get(o + bb_pos) & 0xFF : 0;
+  }
+
+  public Geometry parts(int j) {
+    return parts(new Geometry(), j);
+  }
+
+  public Geometry parts(Geometry obj, int j) {
+    int o = __offset(18);
+    return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null;
+  }
+
+  public int partsLength() {
+    int o = __offset(18);
+    return o != 0 ? __vector_len(o) : 0;
+  }
+
+  public Geometry.Vector partsVector() {
+    return partsVector(new Geometry.Vector());
+  }
+
+  public Geometry.Vector partsVector(Geometry.Vector obj) {
+    int o = __offset(18);
+    return o != 0 ? obj.__assign(__vector(o), 4, bb) : null;
+  }
+
+  public static int createGeometry(FlatBufferBuilder builder,
+      int endsOffset,
+      int xyOffset,
+      int zOffset,
+      int mOffset,
+      int tOffset,
+      int tmOffset,
+      int type,
+      int partsOffset) {
+    builder.startTable(8);
+    Geometry.addParts(builder, partsOffset);
+    Geometry.addTm(builder, tmOffset);
+    Geometry.addT(builder, tOffset);
+    Geometry.addM(builder, mOffset);
+    Geometry.addZ(builder, zOffset);
+    Geometry.addXy(builder, xyOffset);
+    Geometry.addEnds(builder, endsOffset);
+    Geometry.addType(builder, type);
+    return Geometry.endGeometry(builder);
+  }
+
+  public static void startGeometry(FlatBufferBuilder builder) {
+    builder.startTable(8);
+  }
+
+  public static void addEnds(FlatBufferBuilder builder, int endsOffset) {
+    builder.addOffset(0, endsOffset, 0);
+  }
+
+  public static int createEndsVector(FlatBufferBuilder builder, long[] data) {
+    builder.startVector(4, data.length, 4);
+    for (int i = data.length - 1; i >= 0; i--)
+      builder.addInt((int) data[i]);
+    return builder.endVector();
+  }
+
+  public static void startEndsVector(FlatBufferBuilder builder, int numElems) {
+    builder.startVector(4, numElems, 4);
+  }
+
+  public static void addXy(FlatBufferBuilder builder, int xyOffset) {
+    builder.addOffset(1, xyOffset, 0);
+  }
+
+  public static int createXyVector(FlatBufferBuilder builder, double[] data) {
+    builder.startVector(8, data.length, 8);
+    for (int i = data.length - 1; i >= 0; i--)
+      builder.addDouble(data[i]);
+    return builder.endVector();
+  }
+
+  public static void startXyVector(FlatBufferBuilder builder, int numElems) {
+    builder.startVector(8, numElems, 8);
+  }
+
+  public static void addZ(FlatBufferBuilder builder, int zOffset) {
+    builder.addOffset(2, zOffset, 0);
+  }
+
+  public static int createZVector(FlatBufferBuilder builder, double[] data) {
+    builder.startVector(8, data.length, 8);
+    for (int i = data.length - 1; i >= 0; i--)
+      builder.addDouble(data[i]);
+    return builder.endVector();
+  }
+
+  public static void startZVector(FlatBufferBuilder builder, int numElems) {
+    builder.startVector(8, numElems, 8);
+  }
+
+  public static void addM(FlatBufferBuilder builder, int mOffset) {
+    builder.addOffset(3, mOffset, 0);
+  }
+
+  public static int createMVector(FlatBufferBuilder builder, double[] data) {
+    builder.startVector(8, data.length, 8);
+    for (int i = data.length - 1; i >= 0; i--)
+      builder.addDouble(data[i]);
+    return builder.endVector();
+  }
+
+  public static void startMVector(FlatBufferBuilder builder, int numElems) {
+    builder.startVector(8, numElems, 8);
+  }
+
+  public static void addT(FlatBufferBuilder builder, int tOffset) {
+    builder.addOffset(4, tOffset, 0);
+  }
+
+  public static int createTVector(FlatBufferBuilder builder, double[] data) {
+    builder.startVector(8, data.length, 8);
+    for (int i = data.length - 1; i >= 0; i--)
+      builder.addDouble(data[i]);
+    return builder.endVector();
+  }
+
+  public static void startTVector(FlatBufferBuilder builder, int numElems) {
+    builder.startVector(8, numElems, 8);
+  }
+
+  public static void addTm(FlatBufferBuilder builder, int tmOffset) {
+    builder.addOffset(5, tmOffset, 0);
+  }
+
+  public static int createTmVector(FlatBufferBuilder builder, long[] data) {
+    builder.startVector(8, data.length, 8);
+    for (int i = data.length - 1; i >= 0; i--)
+      builder.addLong(data[i]);
+    return builder.endVector();
+  }
+
+  public static void startTmVector(FlatBufferBuilder builder, int numElems) {
+    builder.startVector(8, numElems, 8);
+  }
+
+  public static void addType(FlatBufferBuilder builder, int type) {
+    builder.addByte(6, (byte) type, (byte) 0);
+  }
+
+  public static void addParts(FlatBufferBuilder builder, int partsOffset) {
+    builder.addOffset(7, partsOffset, 0);
+  }
+
+  public static int createPartsVector(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 startPartsVector(FlatBufferBuilder builder, int numElems) 
{
+    builder.startVector(4, numElems, 4);
+  }
+
+  public static int endGeometry(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 Geometry get(int j) {
+      return get(new Geometry(), j);
+    }
+
+    public Geometry get(Geometry obj, int j) {
+      return obj.__assign(__indirect(__element(j), bb), bb);
+    }
+  }
+}
diff --git 
a/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/generated/GeometryType.java
 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/generated/GeometryType.java
new file mode 100644
index 00000000..82584c6f
--- /dev/null
+++ 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/generated/GeometryType.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.baremaps.flatgeobuf.generated;
+
+@SuppressWarnings("unused")
+public final class GeometryType {
+  private GeometryType() {}
+
+  public static final int Unknown = 0;
+  public static final int Point = 1;
+  public static final int LineString = 2;
+  public static final int Polygon = 3;
+  public static final int MultiPoint = 4;
+  public static final int MultiLineString = 5;
+  public static final int MultiPolygon = 6;
+  public static final int GeometryCollection = 7;
+  public static final int CircularString = 8;
+  public static final int CompoundCurve = 9;
+  public static final int CurvePolygon = 10;
+  public static final int MultiCurve = 11;
+  public static final int MultiSurface = 12;
+  public static final int Curve = 13;
+  public static final int Surface = 14;
+  public static final int PolyhedralSurface = 15;
+  public static final int TIN = 16;
+  public static final int Triangle = 17;
+
+  public static final String[] names =
+      {"Unknown", "Point", "LineString", "Polygon", "MultiPoint", 
"MultiLineString", "MultiPolygon",
+          "GeometryCollection", "CircularString", "CompoundCurve", 
"CurvePolygon", "MultiCurve",
+          "MultiSurface", "Curve", "Surface", "PolyhedralSurface", "TIN", 
"Triangle",};
+
+  public static String name(int e) {
+    return names[e];
+  }
+}
diff --git 
a/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/generated/Header.java
 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/generated/Header.java
new file mode 100644
index 00000000..d4d17ee9
--- /dev/null
+++ 
b/baremaps-flatgeobuf/src/main/java/org/apache/baremaps/flatgeobuf/generated/Header.java
@@ -0,0 +1,340 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.baremaps.flatgeobuf.generated;
+
+import com.google.flatbuffers.BaseVector;
+import com.google.flatbuffers.Constants;
+import com.google.flatbuffers.DoubleVector;
+import com.google.flatbuffers.FlatBufferBuilder;
+import com.google.flatbuffers.Table;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@SuppressWarnings("unused")
+public final class Header extends Table {
+  public static void ValidateVersion() {
+    Constants.FLATBUFFERS_24_3_25();
+  }
+
+  public static Header getRootAsHeader(ByteBuffer _bb) {
+    return getRootAsHeader(_bb, new Header());
+  }
+
+  public static Header getRootAsHeader(ByteBuffer _bb, Header 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 Header __assign(int _i, ByteBuffer _bb) {
+    __init(_i, _bb);
+    return this;
+  }
+
+  public String name() {
+    int o = __offset(4);
+    return o != 0 ? __string(o + bb_pos) : null;
+  }
+
+  public ByteBuffer nameAsByteBuffer() {
+    return __vector_as_bytebuffer(4, 1);
+  }
+
+  public ByteBuffer nameInByteBuffer(ByteBuffer _bb) {
+    return __vector_in_bytebuffer(_bb, 4, 1);
+  }
+
+  public double envelope(int j) {
+    int o = __offset(6);
+    return o != 0 ? bb.getDouble(__vector(o) + j * 8) : 0;
+  }
+
+  public int envelopeLength() {
+    int o = __offset(6);
+    return o != 0 ? __vector_len(o) : 0;
+  }
+
+  public DoubleVector envelopeVector() {
+    return envelopeVector(new DoubleVector());
+  }
+
+  public DoubleVector envelopeVector(DoubleVector obj) {
+    int o = __offset(6);
+    return o != 0 ? obj.__assign(__vector(o), bb) : null;
+  }
+
+  public ByteBuffer envelopeAsByteBuffer() {
+    return __vector_as_bytebuffer(6, 8);
+  }
+
+  public ByteBuffer envelopeInByteBuffer(ByteBuffer _bb) {
+    return __vector_in_bytebuffer(_bb, 6, 8);
+  }
+
+  public int geometryType() {
+    int o = __offset(8);
+    return o != 0 ? bb.get(o + bb_pos) & 0xFF : 0;
+  }
+
+  public boolean hasZ() {
+    int o = __offset(10);
+    return o != 0 ? 0 != bb.get(o + bb_pos) : false;
+  }
+
+  public boolean hasM() {
+    int o = __offset(12);
+    return o != 0 ? 0 != bb.get(o + bb_pos) : false;
+  }
+
+  public boolean hasT() {
+    int o = __offset(14);
+    return o != 0 ? 0 != bb.get(o + bb_pos) : false;
+  }
+
+  public boolean hasTm() {
+    int o = __offset(16);
+    return o != 0 ? 0 != bb.get(o + bb_pos) : false;
+  }
+
+  public Column columns(int j) {
+    return columns(new Column(), j);
+  }
+
+  public Column columns(Column obj, int j) {
+    int o = __offset(18);
+    return o != 0 ? obj.__assign(__indirect(__vector(o) + j * 4), bb) : null;
+  }
+
+  public int columnsLength() {
+    int o = __offset(18);
+    return o != 0 ? __vector_len(o) : 0;
+  }
+
+  public Column.Vector columnsVector() {
+    return columnsVector(new Column.Vector());
+  }
+
+  public Column.Vector columnsVector(Column.Vector obj) {
+    int o = __offset(18);
+    return o != 0 ? obj.__assign(__vector(o), 4, bb) : null;
+  }
+
+  public long featuresCount() {
+    int o = __offset(20);
+    return o != 0 ? bb.getLong(o + bb_pos) : 0L;
+  }
+
+  public int indexNodeSize() {
+    int o = __offset(22);
+    return o != 0 ? bb.getShort(o + bb_pos) & 0xFFFF : 16;
+  }
+
+  public Crs crs() {
+    return crs(new Crs());
+  }
+
+  public Crs crs(Crs obj) {
+    int o = __offset(24);
+    return o != 0 ? obj.__assign(__indirect(o + bb_pos), bb) : null;
+  }
+
+  public String title() {
+    int o = __offset(26);
+    return o != 0 ? __string(o + bb_pos) : null;
+  }
+
+  public ByteBuffer titleAsByteBuffer() {
+    return __vector_as_bytebuffer(26, 1);
+  }
+
+  public ByteBuffer titleInByteBuffer(ByteBuffer _bb) {
+    return __vector_in_bytebuffer(_bb, 26, 1);
+  }
+
+  public String description() {
+    int o = __offset(28);
+    return o != 0 ? __string(o + bb_pos) : null;
+  }
+
+  public ByteBuffer descriptionAsByteBuffer() {
+    return __vector_as_bytebuffer(28, 1);
+  }
+
+  public ByteBuffer descriptionInByteBuffer(ByteBuffer _bb) {
+    return __vector_in_bytebuffer(_bb, 28, 1);
+  }
+
+  public String metadata() {
+    int o = __offset(30);
+    return o != 0 ? __string(o + bb_pos) : null;
+  }
+
+  public ByteBuffer metadataAsByteBuffer() {
+    return __vector_as_bytebuffer(30, 1);
+  }
+
+  public ByteBuffer metadataInByteBuffer(ByteBuffer _bb) {
+    return __vector_in_bytebuffer(_bb, 30, 1);
+  }
+
+  public static int createHeader(FlatBufferBuilder builder,
+      int nameOffset,
+      int envelopeOffset,
+      int geometryType,
+      boolean hasZ,
+      boolean hasM,
+      boolean hasT,
+      boolean hasTm,
+      int columnsOffset,
+      long featuresCount,
+      int indexNodeSize,
+      int crsOffset,
+      int titleOffset,
+      int descriptionOffset,
+      int metadataOffset) {
+    builder.startTable(14);
+    Header.addFeaturesCount(builder, featuresCount);
+    Header.addMetadata(builder, metadataOffset);
+    Header.addDescription(builder, descriptionOffset);
+    Header.addTitle(builder, titleOffset);
+    Header.addCrs(builder, crsOffset);
+    Header.addColumns(builder, columnsOffset);
+    Header.addEnvelope(builder, envelopeOffset);
+    Header.addName(builder, nameOffset);
+    Header.addIndexNodeSize(builder, indexNodeSize);
+    Header.addHasTm(builder, hasTm);
+    Header.addHasT(builder, hasT);
+    Header.addHasM(builder, hasM);
+    Header.addHasZ(builder, hasZ);
+    Header.addGeometryType(builder, geometryType);
+    return Header.endHeader(builder);
+  }
+
+  public static void startHeader(FlatBufferBuilder builder) {
+    builder.startTable(14);
+  }
+
+  public static void addName(FlatBufferBuilder builder, int nameOffset) {
+    builder.addOffset(0, nameOffset, 0);
+  }
+
+  public static void addEnvelope(FlatBufferBuilder builder, int 
envelopeOffset) {
+    builder.addOffset(1, envelopeOffset, 0);
+  }
+
+  public static int createEnvelopeVector(FlatBufferBuilder builder, double[] 
data) {
+    builder.startVector(8, data.length, 8);
+    for (int i = data.length - 1; i >= 0; i--)
+      builder.addDouble(data[i]);
+    return builder.endVector();
+  }
+
+  public static void startEnvelopeVector(FlatBufferBuilder builder, int 
numElems) {
+    builder.startVector(8, numElems, 8);
+  }
+
+  public static void addGeometryType(FlatBufferBuilder builder, int 
geometryType) {
+    builder.addByte(2, (byte) geometryType, (byte) 0);
+  }
+
+  public static void addHasZ(FlatBufferBuilder builder, boolean hasZ) {
+    builder.addBoolean(3, hasZ, false);
+  }
+
+  public static void addHasM(FlatBufferBuilder builder, boolean hasM) {
+    builder.addBoolean(4, hasM, false);
+  }
+
+  public static void addHasT(FlatBufferBuilder builder, boolean hasT) {
+    builder.addBoolean(5, hasT, false);
+  }
+
+  public static void addHasTm(FlatBufferBuilder builder, boolean hasTm) {
+    builder.addBoolean(6, hasTm, false);
+  }
+
+  public static void addColumns(FlatBufferBuilder builder, int columnsOffset) {
+    builder.addOffset(7, columnsOffset, 0);
+  }
+
+  public static int createColumnsVector(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 startColumnsVector(FlatBufferBuilder builder, int 
numElems) {
+    builder.startVector(4, numElems, 4);
+  }
+
+  public static void addFeaturesCount(FlatBufferBuilder builder, long 
featuresCount) {
+    builder.addLong(8, featuresCount, 0L);
+  }
+
+  public static void addIndexNodeSize(FlatBufferBuilder builder, int 
indexNodeSize) {
+    builder.addShort(9, (short) indexNodeSize, (short) 16);
+  }
+
+  public static void addCrs(FlatBufferBuilder builder, int crsOffset) {
+    builder.addOffset(10, crsOffset, 0);
+  }
+
+  public static void addTitle(FlatBufferBuilder builder, int titleOffset) {
+    builder.addOffset(11, titleOffset, 0);
+  }
+
+  public static void addDescription(FlatBufferBuilder builder, int 
descriptionOffset) {
+    builder.addOffset(12, descriptionOffset, 0);
+  }
+
+  public static void addMetadata(FlatBufferBuilder builder, int 
metadataOffset) {
+    builder.addOffset(13, metadataOffset, 0);
+  }
+
+  public static int endHeader(FlatBufferBuilder builder) {
+    int o = builder.endTable();
+    return o;
+  }
+
+  public static void finishHeaderBuffer(FlatBufferBuilder builder, int offset) 
{
+    builder.finish(offset);
+  }
+
+  public static void finishSizePrefixedHeaderBuffer(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 Header get(int j) {
+      return get(new Header(), j);
+    }
+
+    public Header get(Header obj, int j) {
+      return obj.__assign(__indirect(__element(j), bb), bb);
+    }
+  }
+}
diff --git a/baremaps-flatgeobuf/src/main/resources/fbs/feature.fbs 
b/baremaps-flatgeobuf/src/main/resources/fbs/feature.fbs
new file mode 100644
index 00000000..b9a874dc
--- /dev/null
+++ b/baremaps-flatgeobuf/src/main/resources/fbs/feature.fbs
@@ -0,0 +1,22 @@
+include "header.fbs";
+
+namespace FlatGeobuf;
+
+table Geometry {
+  ends: [uint];          // Array of end index in flat coordinates per 
geometry part with exterior ring first (NOTE: allowed and recommended to be 
null if 1 part)
+  xy: [double];          // Flat x and y coordinate array (flat pairs) (should 
not be null or empty)
+  z: [double];           // Flat z height array (should not be null or empty 
if header hasZ)
+  m: [double];           // Flat m measurement array (should not be null or 
empty if header hasM)
+  t: [double];           // Flat t geodetic decimal year time array (should 
not be null or empty if header hasT)
+  tm: [ulong];           // Flat tm time nanosecond measurement array (should 
not be null or empty if header hasTM)
+  type: GeometryType;    // Type of geometry (only for elements in 
heterogeneous collection types or if unknown in header)
+  parts: [Geometry];     // Array of parts (for heterogeneous collection types)
+}
+
+table Feature {
+  geometry: Geometry;  // Geometry
+  properties: [ubyte]; // Custom buffer, variable length collection of 
key/value pairs (key=ushort)
+  columns: [Column];   // Attribute columns schema (optional)
+}
+
+root_type Feature;
\ No newline at end of file
diff --git a/baremaps-flatgeobuf/src/main/resources/fbs/header.fbs 
b/baremaps-flatgeobuf/src/main/resources/fbs/header.fbs
new file mode 100644
index 00000000..b1e716c5
--- /dev/null
+++ b/baremaps-flatgeobuf/src/main/resources/fbs/header.fbs
@@ -0,0 +1,84 @@
+namespace FlatGeobuf;
+
+// Geometry type enumeration
+// NOTE: Same as WKB 2D geometry type enumeration
+enum GeometryType: ubyte {
+  Unknown = 0,
+  Point = 1,
+  LineString = 2,
+  Polygon = 3,
+  MultiPoint = 4,
+  MultiLineString = 5,
+  MultiPolygon = 6,
+  GeometryCollection = 7,
+  CircularString = 8,
+  CompoundCurve = 9,
+  CurvePolygon = 10,
+  MultiCurve = 11,
+  MultiSurface = 12,
+  Curve = 13,
+  Surface = 14,
+  PolyhedralSurface = 15,
+  TIN = 16,
+  Triangle = 17
+}
+
+enum ColumnType: ubyte {
+  Byte,                         // Signed 8-bit integer
+  UByte,                        // Unsigned 8-bit integer
+  Bool,                         // Boolean
+  Short,                        // Signed 16-bit integer
+  UShort,                       // Unsigned 16-bit integer
+  Int,                          // Signed 32-bit integer
+  UInt,                         // Unsigned 32-bit integer
+  Long,                         // Signed 64-bit integer
+  ULong,                        // Unsigned 64-bit integer
+  Float,                        // Single precision floating point number
+  Double,                       // Double precision floating point number
+  String,                       // UTF8 string
+  Json,                         // General JSON type intended to be 
application specific
+  DateTime,                     // ISO 8601 date time
+  Binary                        // General binary type intended to be 
application specific
+}
+
+table Column {
+  name: string (required);      // Column name
+  type: ColumnType;             // Column type
+  title: string;                // Column title
+  description: string;          // Column description (intended for free form 
long text)
+  width: int = -1;              // Column values expected width (-1 = unknown) 
(currently only used to indicate the number of characters in strings)
+  precision: int = -1;          // Column values expected precision (-1 = 
unknown) as defined by SQL
+  scale: int = -1;              // Column values expected scale (-1 = unknown) 
as defined by SQL
+  nullable: bool = true;        // Column values expected nullability
+  unique: bool = false;         // Column values expected uniqueness
+  primary_key: bool = false;    // Indicates this column has been (part of) a 
primary key
+  metadata: string;             // Column metadata (intended to be application 
specific and suggested to be structured fx. JSON)
+}
+
+table Crs {
+  org: string;                  // Case-insensitive name of the defining 
organization e.g. EPSG or epsg (NULL = EPSG)
+  code: int;                    // Numeric ID of the Spatial Reference System 
assigned by the organization (0 = unknown)
+  name: string;                 // Human readable name of this SRS
+  description: string;          // Human readable description of this SRS
+  wkt: string;                  // Well-known Text Representation of the 
Spatial Reference System
+  code_string: string;          // Text ID of the Spatial Reference System 
assigned by the organization in the (rare) case when it is not an integer and 
thus cannot be set into code
+}
+
+table Header {
+  name: string;                 // Dataset name
+  envelope: [double];           // Bounds
+  geometry_type: GeometryType;  // Geometry type (should be set to Unknown if 
per feature geometry type)
+  has_z: bool = false;           // Does geometry have Z dimension?
+  has_m: bool = false;           // Does geometry have M dimension?
+  has_t: bool = false;           // Does geometry have T dimension?
+  has_tm: bool = false;          // Does geometry have TM dimension?
+  columns: [Column];            // Attribute columns schema (can be omitted if 
per feature schema)
+  features_count: ulong;        // Number of features in the dataset (0 = 
unknown)
+  index_node_size: ushort = 16; // Index node size (0 = no index)
+  crs: Crs;                     // Spatial Reference System
+  title: string;                // Dataset title
+  description: string;          // Dataset description (intended for free form 
long text)
+  metadata: string;             // Dataset metadata (intended to be 
application specific and suggested to be structured fx. JSON)
+}
+
+root_type Header;
\ No newline at end of file
diff --git 
a/baremaps-flatgeobuf/src/test/java/org/apache/baremaps/flatgeobuf/BufferUtilTest.java
 
b/baremaps-flatgeobuf/src/test/java/org/apache/baremaps/flatgeobuf/BufferUtilTest.java
new file mode 100644
index 00000000..54906543
--- /dev/null
+++ 
b/baremaps-flatgeobuf/src/test/java/org/apache/baremaps/flatgeobuf/BufferUtilTest.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.baremaps.flatgeobuf;
+
+import static org.junit.Assert.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import org.junit.jupiter.api.Test;
+
+public class BufferUtilTest {
+
+  @Test
+  void testPrepareBufferAlreadySufficient() throws IOException {
+    byte[] data = "Hello, World!".getBytes();
+    ReadableByteChannel channel = Channels.newChannel(new 
ByteArrayInputStream(data));
+    ByteBuffer buffer = ByteBuffer.allocate(15);
+    buffer.put(data);
+    buffer.flip();
+
+    ByteBuffer result = BufferUtil.readBytes(channel, buffer, 5);
+    assertEquals(buffer, result);
+    assertEquals(13, result.remaining());
+  }
+
+  @Test
+  void testPrepareBufferCompactAndRead() throws IOException {
+    byte[] data = "Hello, World!".getBytes();
+    ReadableByteChannel channel = Channels.newChannel(new 
ByteArrayInputStream(data));
+    ByteBuffer buffer = ByteBuffer.allocate(15);
+    buffer.put("Hello".getBytes());
+    buffer.flip();
+
+    ByteBuffer result = BufferUtil.readBytes(channel, buffer, 10);
+    assertEquals(buffer, result);
+    assertTrue(result.remaining() >= 10);
+  }
+
+  @Test
+  void testPrepareBufferAllocateNewBuffer() throws IOException {
+    byte[] data = "Hello, World!".getBytes();
+    ReadableByteChannel channel = Channels.newChannel(new 
ByteArrayInputStream(data));
+    ByteBuffer buffer = ByteBuffer.allocate(5);
+    buffer.put("Hi".getBytes());
+    buffer.flip();
+
+    ByteBuffer result = BufferUtil.readBytes(channel, buffer, 10);
+    assertNotEquals(buffer, result);
+    assertTrue(result.capacity() >= 10);
+    assertTrue(result.remaining() >= 10);
+  }
+
+  @Test
+  void testPrepareBufferWithExactCapacity() throws IOException {
+    byte[] data = "Hello, World!".getBytes();
+    ReadableByteChannel channel = Channels.newChannel(new 
ByteArrayInputStream(data));
+    ByteBuffer buffer = ByteBuffer.allocate(13);
+    buffer.put(data, 0, 5);
+    buffer.flip();
+
+    ByteBuffer result = BufferUtil.readBytes(channel, buffer, 10);
+    assertEquals(buffer, result);
+    assertTrue(result.remaining() >= 10);
+  }
+
+  @Test
+  void testPrepareEndOfChannel() throws IOException {
+    byte[] data = "Hello".getBytes();
+    ReadableByteChannel channel = Channels.newChannel(new 
ByteArrayInputStream(data));
+    ByteBuffer buffer = ByteBuffer.allocate(10);
+    buffer.put("Hi".getBytes());
+    buffer.flip();
+
+    ByteBuffer result = BufferUtil.readBytes(channel, buffer, 10);
+    assertEquals(buffer, result);
+    assertTrue(result.remaining() <= 10);
+  }
+
+  @Test
+  void testPrepareNullChannel() {
+    ByteBuffer buffer = ByteBuffer.allocate(10);
+    IllegalArgumentException thrown = 
assertThrows(IllegalArgumentException.class, () -> {
+      BufferUtil.readBytes(null, buffer, 10);
+    });
+    assertEquals("Channel and buffer must not be null", thrown.getMessage());
+  }
+
+  @Test
+  void testPrepareNullBuffer() {
+    byte[] data = "Hello".getBytes();
+    ReadableByteChannel channel = Channels.newChannel(new 
ByteArrayInputStream(data));
+    IllegalArgumentException thrown = 
assertThrows(IllegalArgumentException.class, () -> {
+      BufferUtil.readBytes(channel, null, 10);
+    });
+    assertEquals("Channel and buffer must not be null", thrown.getMessage());
+  }
+
+  @Test
+  void testPrepareNegativeBytes() {
+    byte[] data = "Hello".getBytes();
+    ReadableByteChannel channel = Channels.newChannel(new 
ByteArrayInputStream(data));
+    ByteBuffer buffer = ByteBuffer.allocate(10);
+    IllegalArgumentException thrown = 
assertThrows(IllegalArgumentException.class, () -> {
+      BufferUtil.readBytes(channel, buffer, -1);
+    });
+    assertEquals("The number of bytes to read must be non-negative", 
thrown.getMessage());
+  }
+
+}
diff --git 
a/baremaps-flatgeobuf/src/test/java/org/apache/baremaps/flatgeobuf/FlatGeoBufTest.java
 
b/baremaps-flatgeobuf/src/test/java/org/apache/baremaps/flatgeobuf/FlatGeoBufTest.java
new file mode 100644
index 00000000..03401dd5
--- /dev/null
+++ 
b/baremaps-flatgeobuf/src/test/java/org/apache/baremaps/flatgeobuf/FlatGeoBufTest.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.baremaps.flatgeobuf;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.channels.FileChannel;
+import java.nio.file.StandardOpenOption;
+import org.apache.baremaps.flatgeobuf.generated.Feature;
+import org.apache.baremaps.flatgeobuf.generated.Header;
+import org.apache.baremaps.testing.TestFiles;
+import org.junit.jupiter.api.Test;
+
+public class FlatGeoBufTest {
+
+  @Test
+  void readHeader() throws IOException {
+    var file = 
TestFiles.resolve("baremaps-testing/data/samples/countries.fgb");
+    try (var channel = FileChannel.open(file, StandardOpenOption.READ)) {
+      Header header = FlatGeoBufReader.readHeader(channel);
+      assertNotNull(header);
+      assertEquals(179, header.featuresCount());
+    }
+  }
+
+  @Test
+  void readFeature() throws IOException {
+    var file = 
TestFiles.resolve("baremaps-testing/data/samples/countries.fgb");
+    try (var channel = FileChannel.open(file, StandardOpenOption.READ)) {
+
+      // Read the header
+      Header header = FlatGeoBufReader.readHeader(channel);
+      assertNotNull(header);
+      assertEquals(179, header.featuresCount());
+
+      // Read the index
+      FlatGeoBufReader.skipIndex(channel, header);
+
+      // Read the first feature
+      ByteBuffer buffer = BufferUtil.createByteBuffer(1 << 16, 
ByteOrder.LITTLE_ENDIAN);
+      for (int i = 0; i < header.featuresCount(); i++) {
+        Feature feature = FlatGeoBufReader.readFeature(channel, buffer);
+
+        System.out.println(FlatGeoBuf.asFeatureRecord(header, feature));
+
+        assertNotNull(feature);
+      }
+    }
+  }
+
+
+  public static void main(String... args) {
+    System.out.println(Long.MAX_VALUE);
+    System.out.println(Long.MAX_VALUE >> 32);
+  }
+
+}
diff --git a/baremaps-testing/pom.xml b/baremaps-testing/pom.xml
index c2578470..ac8075f4 100644
--- a/baremaps-testing/pom.xml
+++ b/baremaps-testing/pom.xml
@@ -10,4 +10,10 @@
   <properties>
     <maven.deploy.skip>true</maven.deploy.skip>
   </properties>
+  <dependencies>
+    <dependency>
+      <groupId>org.locationtech.jts</groupId>
+      <artifactId>jts-core</artifactId>
+    </dependency>
+  </dependencies>
 </project>
diff --git a/pom.xml b/pom.xml
index eb1e3bd5..4d25e7c4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -52,6 +52,7 @@ limitations under the License.
     <module>baremaps-cli</module>
     <module>baremaps-core</module>
     <module>baremaps-data</module>
+    <module>baremaps-flatgeobuf</module>
     <module>baremaps-geoparquet</module>
     <module>baremaps-maplibre</module>
     <module>baremaps-openstreetmap</module>
@@ -526,10 +527,6 @@ limitations under the License.
       <groupId>org.testcontainers</groupId>
       <artifactId>testcontainers</artifactId>
     </dependency>
-    <dependency>
-      <groupId>org.wololo</groupId>
-      <artifactId>flatgeobuf</artifactId>
-    </dependency>
   </dependencies>
 
   <build>
diff --git a/scripts/generate-flatgeobuf.sh b/scripts/generate-flatgeobuf.sh
new file mode 100755
index 00000000..ea5317da
--- /dev/null
+++ b/scripts/generate-flatgeobuf.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+flatc --java --gen-all \
+  --java-package-prefix org.apache.baremaps.flatgeobuf.generated \
+  -o baremaps-flatgeobuf/src/main/java \
+  baremaps-flatgeobuf/src/main/resources/fbs/*.fbs

Reply via email to