You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@baremaps.apache.org by bc...@apache.org on 2023/01/12 22:57:45 UTC

[incubator-baremaps] 01/04: Add feature abstractions

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

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

commit 965c558a378e465b21e2620202c32d09dc5fe709
Author: Bertil Chapuis <bc...@gmail.com>
AuthorDate: Sat Jan 7 20:59:59 2023 +0100

    Add feature abstractions
---
 .../apache/baremaps/cli/database/ExecuteSql.java   |   2 +-
 .../baremaps/cli/database/ImportOpenStreetMap.java |   2 +-
 .../java/org/apache/baremaps/cli/map/Export.java   |   4 +-
 .../java/org/apache/baremaps/cli/map/Init.java     |   6 +-
 .../java/org/apache/baremaps/cli/map/Serve.java    |   2 +-
 baremaps-core/pom.xml                              |  21 +-
 .../baremaps/collection/AlignedDataList.java       |   9 +
 .../org/apache/baremaps/collection/DataList.java   |  16 +-
 .../baremaps/collection/IndexedDataList.java       |   7 +
 .../apache/baremaps/collection/SizedDataList.java  |   7 +
 .../baremaps/collection/type/NullableDataType.java |  50 +++
 .../collection/utils/CollectionAdapter.java        | 127 ++++++
 .../apache/baremaps/database/ChangeImporter.java   |   6 +-
 .../org/apache/baremaps/database/DiffService.java  |   6 +-
 .../database/repository/PostgresJsonbMapper.java   |   6 +-
 .../repository/PostgresNodeRepository.java         |   8 +-
 .../repository/PostgresRelationRepository.java     |  13 +-
 .../database/repository/PostgresWayRepository.java |   8 +-
 .../baremaps/database/tile/PostgresTileStore.java  |   2 +-
 .../ConsumerUtils.java => feature/Aggregate.java}  |  26 +-
 .../ConsumerUtils.java => feature/Feature.java}    |  29 +-
 .../FeatureCollection.java}                        |  24 +-
 .../org/apache/baremaps/feature/FeatureImpl.java   |  55 +++
 .../ConsumerUtils.java => feature/FeatureSet.java} |  23 +-
 .../org/apache/baremaps/feature/FeatureType.java   |  47 +++
 .../PropertyType.java}                             |  33 +-
 .../ReadableAggregate.java}                        |  24 +-
 .../ReadableFeatureSet.java}                       |  24 +-
 .../baremaps/feature/ReflectiveFeatureAdapter.java |  90 ++++
 .../ConsumerUtils.java => feature/Resource.java}   |  23 +-
 .../WritableAggregate.java}                        |  25 +-
 .../WritableFeatureSet.java}                       |  24 +-
 .../java/org/apache/baremaps/mvt/Expressions.java  | 469 +++++++++++++++++++++
 .../org/apache/baremaps/{ => mvt}/style/Style.java |   2 +-
 .../baremaps/{ => mvt}/style/StyleLayer.java       |   2 +-
 .../baremaps/{ => mvt}/style/StyleSource.java      |   2 +-
 .../apache/baremaps/{ => mvt}/tileset/Tileset.java |   2 +-
 .../baremaps/{ => mvt}/tileset/TilesetLayer.java   |   2 +-
 .../baremaps/{ => mvt}/tileset/TilesetQuery.java   |   2 +-
 .../function/CoordinateMapBuilder.java             |   2 +-
 .../openstreetmap/function/GeometryMapBuilder.java |   2 +-
 .../function/ReferenceMapBuilder.java              |   2 +-
 .../function/RelationGeometryBuilder.java          |   4 +-
 .../openstreetmap/function/WayGeometryBuilder.java |   2 +-
 .../baremaps/openstreetmap/model/Element.java      |  11 +-
 .../apache/baremaps/openstreetmap/model/Node.java  |   4 +-
 .../baremaps/openstreetmap/model/Relation.java     |   4 +-
 .../apache/baremaps/openstreetmap/model/Way.java   |   4 +-
 .../openstreetmap/pbf/DataBlockReader.java         |  12 +-
 .../openstreetmap/xml/XmlChangeSpliterator.java    |   8 +-
 .../openstreetmap/xml/XmlEntitySpliterator.java    |   8 +-
 .../storage/FeatureSetProjectionTransform.java     |  65 +--
 .../storage/geopackage/GeoPackageDatabase.java     |  44 +-
 .../storage/geopackage/GeoPackageTable.java        |  73 +---
 .../storage/postgres/PostgresDatabase.java         | 121 ++----
 .../baremaps/storage/postgres/PostgresTable.java   |  67 +--
 .../storage/shapefile/ShapefileDirectory.java      |  47 +--
 .../storage/shapefile/ShapefileFeatureSet.java     |  67 +--
 .../shapefile/internal/DbaseByteReader.java        |   6 +-
 .../shapefile/internal/InputFeatureStream.java     |  16 +-
 .../shapefile/internal/ShapefileByteReader.java    |  48 +--
 .../shapefile/internal/ShapefileReader.java        |   6 +-
 .../org/apache/baremaps/stream/ConsumerUtils.java  |  15 +
 .../workflow/tasks/CreateGeonamesIndex.java        |   4 +-
 .../baremaps/workflow/tasks/CreateIplocIndex.java  |   8 +-
 .../baremaps/workflow/tasks/DownloadUrl.java       |   7 +-
 .../apache/baremaps/workflow/tasks/ExecuteSql.java |   5 +-
 .../baremaps/workflow/tasks/ExportVectorTiles.java |  14 +-
 .../baremaps/workflow/tasks/ImportGeoPackage.java  |  18 +-
 .../workflow/tasks/ImportOpenStreetMap.java        |   5 +-
 .../baremaps/workflow/tasks/ImportShapefile.java   |   8 +-
 .../workflow/tasks/SimplifyOpenStreetMap.java      | 150 +++++++
 .../apache/baremaps/workflow/tasks/UngzipFile.java |   7 +-
 .../apache/baremaps/workflow/tasks/UnzipFile.java  |  11 +-
 .../database/PostgresNodeRepositoryTest.java       |  14 +-
 .../database/PostgresRelationRepositoryTest.java   |  14 +-
 .../database/PostgresWayRepositoryTest.java        |  14 +-
 .../database/database/WayRepositoryTest.java       |  14 +-
 .../org/apache/baremaps/mvt/ExpressionsTest.java   | 232 ++++++++++
 .../geometry/EntityGeometryBuilderTest.java        |   4 +-
 .../geometry/RelationGeometryBuilderTest.java      |   4 +-
 .../baremaps/testing/PostgresContainerTest.java    |   2 +-
 .../apache/baremaps/workflow/ObjectMapperTest.java |   5 +-
 .../org/apache/baremaps/workflow/WorkflowTest.java |  45 +-
 .../baremaps/workflow/tasks/DownloadUrlTest.java   |   4 +-
 .../workflow/tasks/ExecuteSqlFileTest.java         |   2 +-
 .../workflow/tasks/ImportGeoPackageTest.java       |   2 +-
 .../workflow/tasks/ImportOpenStreetMapTest.java    |   2 +-
 .../workflow/tasks/ImportShapefileTest.java        |   4 +-
 ...ileTest.java => SimplifyOpenStreetMapTest.java} |  11 +-
 .../baremaps/workflow/tasks/UngzipFileTest.java    |   2 +-
 .../baremaps/workflow/tasks/UnzipFileTest.java     |   2 +-
 .../org/apache/baremaps/server/DevResources.java   |   4 +-
 .../apache/baremaps/server/ServerResources.java    |   6 +-
 pom.xml                                            |  73 ++--
 95 files changed, 1686 insertions(+), 879 deletions(-)

diff --git a/baremaps-cli/src/main/java/org/apache/baremaps/cli/database/ExecuteSql.java b/baremaps-cli/src/main/java/org/apache/baremaps/cli/database/ExecuteSql.java
index 635509b8..cf8348ec 100644
--- a/baremaps-cli/src/main/java/org/apache/baremaps/cli/database/ExecuteSql.java
+++ b/baremaps-cli/src/main/java/org/apache/baremaps/cli/database/ExecuteSql.java
@@ -42,7 +42,7 @@ public class ExecuteSql implements Callable<Integer> {
 
   @Override
   public Integer call() throws Exception {
-    new org.apache.baremaps.workflow.tasks.ExecuteSql(database, file.toAbsolutePath().toString(),
+    new org.apache.baremaps.workflow.tasks.ExecuteSql(database, file.toAbsolutePath(),
         parallel).execute(new WorkflowContext());
     return 0;
   }
diff --git a/baremaps-cli/src/main/java/org/apache/baremaps/cli/database/ImportOpenStreetMap.java b/baremaps-cli/src/main/java/org/apache/baremaps/cli/database/ImportOpenStreetMap.java
index 3ea07c25..b64cb565 100644
--- a/baremaps-cli/src/main/java/org/apache/baremaps/cli/database/ImportOpenStreetMap.java
+++ b/baremaps-cli/src/main/java/org/apache/baremaps/cli/database/ImportOpenStreetMap.java
@@ -42,7 +42,7 @@ public class ImportOpenStreetMap implements Callable<Integer> {
 
   @Override
   public Integer call() throws Exception {
-    new org.apache.baremaps.workflow.tasks.ImportOpenStreetMap(file.toAbsolutePath().toString(),
+    new org.apache.baremaps.workflow.tasks.ImportOpenStreetMap(file.toAbsolutePath(),
         database, srid).execute(new WorkflowContext());
     return 0;
   }
diff --git a/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Export.java b/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Export.java
index c1c49668..119fd872 100644
--- a/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Export.java
+++ b/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Export.java
@@ -63,8 +63,8 @@ public class Export implements Callable<Integer> {
 
   @Override
   public Integer call() throws Exception {
-    new ExportVectorTiles(database, tileset.toAbsolutePath().toString(),
-        repository.toAbsolutePath().toString(), batchArraySize, batchArrayIndex, mbtiles)
+    new ExportVectorTiles(database, tileset.toAbsolutePath(),
+        repository.toAbsolutePath(), batchArraySize, batchArrayIndex, mbtiles)
             .execute(new WorkflowContext());
     return 0;
   }
diff --git a/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Init.java b/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Init.java
index 5e52bbce..e4f3efe5 100644
--- a/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Init.java
+++ b/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Init.java
@@ -20,9 +20,9 @@ import java.util.Arrays;
 import java.util.Map;
 import java.util.concurrent.Callable;
 import org.apache.baremaps.cli.Options;
-import org.apache.baremaps.style.Style;
-import org.apache.baremaps.style.StyleSource;
-import org.apache.baremaps.tileset.Tileset;
+import org.apache.baremaps.mvt.style.Style;
+import org.apache.baremaps.mvt.style.StyleSource;
+import org.apache.baremaps.mvt.tileset.Tileset;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import picocli.CommandLine.Command;
diff --git a/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Serve.java b/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Serve.java
index 7db644dc..05258f1e 100644
--- a/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Serve.java
+++ b/baremaps-cli/src/main/java/org/apache/baremaps/cli/map/Serve.java
@@ -26,10 +26,10 @@ import org.apache.baremaps.database.PostgresUtils;
 import org.apache.baremaps.database.tile.PostgresTileStore;
 import org.apache.baremaps.database.tile.TileCache;
 import org.apache.baremaps.database.tile.TileStore;
+import org.apache.baremaps.mvt.tileset.Tileset;
 import org.apache.baremaps.server.ConfigReader;
 import org.apache.baremaps.server.CorsFilter;
 import org.apache.baremaps.server.ServerResources;
-import org.apache.baremaps.tileset.Tileset;
 import org.glassfish.hk2.utilities.binding.AbstractBinder;
 import org.glassfish.jersey.server.ResourceConfig;
 import org.slf4j.Logger;
diff --git a/baremaps-core/pom.xml b/baremaps-core/pom.xml
index c5cc7fb5..c844c210 100644
--- a/baremaps-core/pom.xml
+++ b/baremaps-core/pom.xml
@@ -98,22 +98,6 @@
       <groupId>org.apache.lucene</groupId>
       <artifactId>lucene-spatial</artifactId>
     </dependency>
-    <dependency>
-      <groupId>org.apache.sis.core</groupId>
-      <artifactId>sis-feature</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.sis.core</groupId>
-      <artifactId>sis-referencing</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.sis.core</groupId>
-      <artifactId>sis-utility</artifactId>
-    </dependency>
-    <dependency>
-      <groupId>org.apache.sis.storage</groupId>
-      <artifactId>sis-storage</artifactId>
-    </dependency>
     <dependency>
       <groupId>org.locationtech.jts</groupId>
       <artifactId>jts-core</artifactId>
@@ -130,6 +114,11 @@
       <groupId>org.slf4j</groupId>
       <artifactId>jul-to-slf4j</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.wololo</groupId>
+      <artifactId>flatgeobuf</artifactId>
+      <version>3.24.0</version>
+    </dependency>
     <dependency>
       <groupId>org.xerial</groupId>
       <artifactId>sqlite-jdbc</artifactId>
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/collection/AlignedDataList.java b/baremaps-core/src/main/java/org/apache/baremaps/collection/AlignedDataList.java
index 327acd8b..aba40eb0 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/collection/AlignedDataList.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/collection/AlignedDataList.java
@@ -14,6 +14,7 @@ package org.apache.baremaps.collection;
 
 
 
+import it.unimi.dsi.fastutil.longs.LongArrayList;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.util.concurrent.atomic.AtomicLong;
@@ -52,6 +53,7 @@ public class AlignedDataList<T> implements DataList<T> {
    * @param memory the memory
    */
   public AlignedDataList(SizedDataType<T> dataType, Memory memory) {
+    new LongArrayList();
     if (dataType.size() > memory.segmentSize()) {
       throw new StoreException("The segment size is too small for the data type");
     }
@@ -98,6 +100,13 @@ public class AlignedDataList<T> implements DataList<T> {
     return dataType.read(segment, segmentOffset);
   }
 
+  @Override
+  public T remove(long index) {
+    T value = get(index);
+    write(index, null);
+    return value;
+  }
+
   /** {@inheritDoc} */
   public long size() {
     return size.get();
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/collection/DataList.java b/baremaps-core/src/main/java/org/apache/baremaps/collection/DataList.java
index df7c2182..f017e8af 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/collection/DataList.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/collection/DataList.java
@@ -26,15 +26,15 @@ public interface DataList<T> extends Closeable, Cleanable {
   /**
    * Adds a value to the list and returns its index.
    *
-   * @param value the value
+   * @param value the value to be added
    * @return the index of the value
    */
   long add(T value);
 
   /**
-   * Inserts the specified element at the specified position in this list.
+   * Inserts the specified value at the specified position in this list.
    *
-   * @param index the index of the value
+   * @param index the index of the value to be added
    * @param value the value
    */
   void add(long index, T value);
@@ -42,11 +42,19 @@ public interface DataList<T> extends Closeable, Cleanable {
   /**
    * Returns a values by its index.
    *
-   * @param index the index of the value
+   * @param index the index of the value to be removed
    * @return the value
    */
   T get(long index);
 
+  /**
+   * Removes the value at the specified position in this list.
+   *
+   * @param index the index of the value to be removed
+   * @return the value that was removed from the list
+   */
+  T remove(long index);
+
   /**
    * Returns the size of the list.
    *
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/collection/IndexedDataList.java b/baremaps-core/src/main/java/org/apache/baremaps/collection/IndexedDataList.java
index fb76c4d6..d7c838f1 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/collection/IndexedDataList.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/collection/IndexedDataList.java
@@ -42,6 +42,13 @@ public class IndexedDataList<T> implements DataList<T> {
     return store.get(index.get(idx));
   }
 
+  @Override
+  public T remove(long idx) {
+    T value = get(idx);
+    index.remove(idx);
+    return value;
+  }
+
   @Override
   public long size() {
     return index.size();
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/collection/SizedDataList.java b/baremaps-core/src/main/java/org/apache/baremaps/collection/SizedDataList.java
index 3d1efb1b..f0cdf6f9 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/collection/SizedDataList.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/collection/SizedDataList.java
@@ -86,6 +86,13 @@ public class SizedDataList<T> implements DataList<T> {
     return dataType.read(segment, segmentOffset);
   }
 
+  @Override
+  public T remove(long index) {
+    T value = get(index);
+    add(index, null);
+    return value;
+  }
+
   /** {@inheritDoc} */
   public long size() {
     return size.get();
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/collection/type/NullableDataType.java b/baremaps-core/src/main/java/org/apache/baremaps/collection/type/NullableDataType.java
new file mode 100644
index 00000000..343dcfda
--- /dev/null
+++ b/baremaps-core/src/main/java/org/apache/baremaps/collection/type/NullableDataType.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed 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.collection.type;
+
+
+
+import java.nio.ByteBuffer;
+
+public class NullableDataType<T> implements DataType<T> {
+
+  private final DataType<T> dataType;
+
+  public NullableDataType(DataType<T> dataType) {
+    this.dataType = dataType;
+  }
+
+  @Override
+  public int size(T value) {
+    return 1 + dataType.size(value);
+  }
+
+  @Override
+  public void write(ByteBuffer buffer, int position, T value) {
+    if (value == null) {
+      buffer.put(position, (byte) 0);
+    } else {
+      buffer.put(position, (byte) 1);
+      dataType.write(buffer, position + 1, value);
+    }
+  }
+
+  @Override
+  public T read(ByteBuffer buffer, int position) {
+    if (buffer.get(position) == 0) {
+      return null;
+    } else {
+      return dataType.read(buffer, position + 1);
+    }
+  }
+}
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/collection/utils/CollectionAdapter.java b/baremaps-core/src/main/java/org/apache/baremaps/collection/utils/CollectionAdapter.java
new file mode 100644
index 00000000..6c691059
--- /dev/null
+++ b/baremaps-core/src/main/java/org/apache/baremaps/collection/utils/CollectionAdapter.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed 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.collection.utils;
+
+
+
+import java.util.Collection;
+import java.util.Iterator;
+import org.apache.baremaps.collection.DataList;
+
+public class CollectionAdapter<T> implements Collection<T> {
+
+  private final DataList<T> dataList;
+
+  public CollectionAdapter(DataList<T> dataList) {
+    this.dataList = dataList;
+  }
+
+  @Override
+  public int size() {
+    if (dataList.size() > Integer.MAX_VALUE) {
+      throw new IllegalStateException(
+          "The collection is too large to be represented as an integer.");
+    }
+    return (int) dataList.size();
+  }
+
+  @Override
+  public boolean isEmpty() {
+    return dataList.size() == 0;
+  }
+
+  @Override
+  public boolean contains(Object o) {
+    for (long i = 0; i < dataList.size(); i++) {
+      if (dataList.get(i).equals(o)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public Iterator<T> iterator() {
+    return new Iterator<>() {
+
+      private long index = 0;
+
+      @Override
+      public boolean hasNext() {
+        return index < dataList.size();
+      }
+
+      @Override
+      public T next() {
+        return dataList.get(index++);
+      }
+    };
+  }
+
+  @Override
+  public Object[] toArray() {
+    return toArray(new Object[size()]);
+  }
+
+  @Override
+  public <T1> T1[] toArray(T1[] a) {
+    for (int i = 0; i < size(); i++) {
+      a[i] = (T1) dataList.get(i);
+    }
+    return a;
+  }
+
+  @Override
+  public boolean add(T t) {
+    dataList.add(t);
+    return true;
+  }
+
+  @Override
+  public boolean remove(Object o) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean containsAll(Collection<?> c) {
+    for (Object o : c) {
+      if (!contains(o)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public boolean addAll(Collection<? extends T> c) {
+    for (T t : c) {
+      add(t);
+    }
+    return true;
+  }
+
+  @Override
+  public boolean removeAll(Collection<?> c) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public boolean retainAll(Collection<?> c) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void clear() {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/database/ChangeImporter.java b/baremaps-core/src/main/java/org/apache/baremaps/database/ChangeImporter.java
index ab91bb2c..2c09d389 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/database/ChangeImporter.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/database/ChangeImporter.java
@@ -62,11 +62,11 @@ public class ChangeImporter implements Consumer<Change> {
             break;
           case DELETE:
             if (entity instanceof Node node) {
-              nodeRepository.delete(node.getId());
+              nodeRepository.delete(node.id());
             } else if (entity instanceof Way way) {
-              wayRepository.delete(way.getId());
+              wayRepository.delete(way.id());
             } else if (entity instanceof Relation relation) {
-              relationRepository.delete(relation.getId());
+              relationRepository.delete(relation.id());
             }
             break;
         }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/database/DiffService.java b/baremaps-core/src/main/java/org/apache/baremaps/database/DiffService.java
index 289c7bdc..14fba498 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/database/DiffService.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/database/DiffService.java
@@ -115,13 +115,13 @@ public class DiffService implements Callable<List<Tile>> {
   private Optional<Geometry> geometriesForPreviousVersion(Entity entity) {
     try {
       if (entity instanceof Node node) {
-        var previousNode = nodeRepository.get(node.getId());
+        var previousNode = nodeRepository.get(node.id());
         return Optional.ofNullable(previousNode).map(Node::getGeometry);
       } else if (entity instanceof Way way) {
-        var previousWay = wayRepository.get(way.getId());
+        var previousWay = wayRepository.get(way.id());
         return Optional.ofNullable(previousWay).map(Way::getGeometry);
       } else if (entity instanceof Relation relation) {
-        var previousRelation = relationRepository.get(relation.getId());
+        var previousRelation = relationRepository.get(relation.id());
         return Optional.ofNullable(previousRelation).map(Relation::getGeometry);
       } else {
         return Optional.empty();
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/database/repository/PostgresJsonbMapper.java b/baremaps-core/src/main/java/org/apache/baremaps/database/repository/PostgresJsonbMapper.java
index 2f130e07..513790e0 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/database/repository/PostgresJsonbMapper.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/database/repository/PostgresJsonbMapper.java
@@ -33,7 +33,7 @@ class PostgresJsonbMapper {
    * @return a Json string representing the object
    * @throws JsonProcessingException
    */
-  public static String toJson(Map<String, String> input) throws JsonProcessingException {
+  public static String toJson(Map<String, Object> input) throws JsonProcessingException {
     return mapper.writeValueAsString(input);
   }
 
@@ -44,8 +44,8 @@ class PostgresJsonbMapper {
    * @return a map with the entry of the objects
    * @throws JsonProcessingException
    */
-  public static Map<String, String> toMap(String input) throws JsonProcessingException {
-    TypeReference<HashMap<String, String>> typeRef = new TypeReference<>() {};
+  public static Map<String, Object> toMap(String input) throws JsonProcessingException {
+    TypeReference<HashMap<String, Object>> typeRef = new TypeReference<>() {};
     return mapper.readValue(input, typeRef);
   }
 }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/database/repository/PostgresNodeRepository.java b/baremaps-core/src/main/java/org/apache/baremaps/database/repository/PostgresNodeRepository.java
index b307d622..eb24cf6d 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/database/repository/PostgresNodeRepository.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/database/repository/PostgresNodeRepository.java
@@ -187,7 +187,7 @@ public class PostgresNodeRepository implements Repository<Long, Node> {
         Map<Long, Node> values = new HashMap<>();
         while (result.next()) {
           Node value = getValue(result);
-          values.put(value.getId(), value);
+          values.put(value.id(), value);
         }
         return keys.stream().map(values::get).toList();
       }
@@ -270,7 +270,7 @@ public class PostgresNodeRepository implements Repository<Long, Node> {
         writer.writeHeader();
         for (Node value : values) {
           writer.startRow(9);
-          writer.writeLong(value.getId());
+          writer.writeLong(value.id());
           writer.writeInteger(value.getInfo().getVersion());
           writer.writeInteger(value.getInfo().getUid());
           writer.writeLocalDateTime(value.getInfo().getTimestamp());
@@ -292,7 +292,7 @@ public class PostgresNodeRepository implements Repository<Long, Node> {
     int uid = resultSet.getInt(3);
     LocalDateTime timestamp = resultSet.getObject(4, LocalDateTime.class);
     long changeset = resultSet.getLong(5);
-    Map<String, String> tags = PostgresJsonbMapper.toMap(resultSet.getString(6));
+    Map<String, Object> tags = PostgresJsonbMapper.toMap(resultSet.getString(6));
     double lon = resultSet.getDouble(7);
     double lat = resultSet.getDouble(8);
     Geometry point = GeometryUtils.deserialize(resultSet.getBytes(9));
@@ -302,7 +302,7 @@ public class PostgresNodeRepository implements Repository<Long, Node> {
 
   private void setValue(PreparedStatement statement, Node value)
       throws SQLException, JsonProcessingException {
-    statement.setObject(1, value.getId());
+    statement.setObject(1, value.id());
     statement.setObject(2, value.getInfo().getVersion());
     statement.setObject(3, value.getInfo().getUid());
     statement.setObject(4, value.getInfo().getTimestamp());
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/database/repository/PostgresRelationRepository.java b/baremaps-core/src/main/java/org/apache/baremaps/database/repository/PostgresRelationRepository.java
index 6d37d1aa..a11483ae 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/database/repository/PostgresRelationRepository.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/database/repository/PostgresRelationRepository.java
@@ -22,10 +22,7 @@ import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.time.LocalDateTime;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.stream.Collectors;
 import javax.sql.DataSource;
 import org.apache.baremaps.database.copy.CopyWriter;
@@ -199,7 +196,7 @@ public class PostgresRelationRepository implements Repository<Long, Relation> {
         Map<Long, Relation> values = new HashMap<>();
         while (result.next()) {
           Relation value = getValue(result);
-          values.put(value.getId(), value);
+          values.put(value.id(), value);
         }
         return keys.stream().map(values::get).toList();
       }
@@ -282,7 +279,7 @@ public class PostgresRelationRepository implements Repository<Long, Relation> {
         writer.writeHeader();
         for (Relation value : values) {
           writer.startRow(10);
-          writer.writeLong(value.getId());
+          writer.writeLong(value.id());
           writer.writeInteger(value.getInfo().getVersion());
           writer.writeInteger(value.getInfo().getUid());
           writer.writeLocalDateTime(value.getInfo().getTimestamp());
@@ -308,7 +305,7 @@ public class PostgresRelationRepository implements Repository<Long, Relation> {
     int uid = resultSet.getInt(3);
     LocalDateTime timestamp = resultSet.getObject(4, LocalDateTime.class);
     long changeset = resultSet.getLong(5);
-    Map<String, String> tags = toMap(resultSet.getString(6));
+    Map<String, Object> tags = toMap(resultSet.getString(6));
     Long[] refs = (Long[]) resultSet.getArray(7).getArray();
     Integer[] types = (Integer[]) resultSet.getArray(8).getArray();
     String[] roles = (String[]) resultSet.getArray(9).getArray();
@@ -323,7 +320,7 @@ public class PostgresRelationRepository implements Repository<Long, Relation> {
 
   private void setValue(PreparedStatement statement, Relation value)
       throws SQLException, JsonProcessingException {
-    statement.setObject(1, value.getId());
+    statement.setObject(1, value.id());
     statement.setObject(2, value.getInfo().getVersion());
     statement.setObject(3, value.getInfo().getUid());
     statement.setObject(4, value.getInfo().getTimestamp());
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/database/repository/PostgresWayRepository.java b/baremaps-core/src/main/java/org/apache/baremaps/database/repository/PostgresWayRepository.java
index 8360f79f..b4c1f624 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/database/repository/PostgresWayRepository.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/database/repository/PostgresWayRepository.java
@@ -190,7 +190,7 @@ public class PostgresWayRepository implements Repository<Long, Way> {
         Map<Long, Way> values = new HashMap<>();
         while (result.next()) {
           Way value = getValue(result);
-          values.put(value.getId(), value);
+          values.put(value.id(), value);
         }
         return keys.stream().map(values::get).toList();
       }
@@ -272,7 +272,7 @@ public class PostgresWayRepository implements Repository<Long, Way> {
         writer.writeHeader();
         for (Way value : values) {
           writer.startRow(8);
-          writer.writeLong(value.getId());
+          writer.writeLong(value.id());
           writer.writeInteger(value.getInfo().getVersion());
           writer.writeInteger(value.getInfo().getUid());
           writer.writeLocalDateTime(value.getInfo().getTimestamp());
@@ -293,7 +293,7 @@ public class PostgresWayRepository implements Repository<Long, Way> {
     int uid = resultSet.getInt(3);
     LocalDateTime timestamp = resultSet.getObject(4, LocalDateTime.class);
     long changeset = resultSet.getLong(5);
-    Map<String, String> tags = PostgresJsonbMapper.toMap(resultSet.getString(6));
+    Map<String, Object> tags = PostgresJsonbMapper.toMap(resultSet.getString(6));
     List<Long> nodes = new ArrayList<>();
     Array array = resultSet.getArray(7);
     if (array != null) {
@@ -306,7 +306,7 @@ public class PostgresWayRepository implements Repository<Long, Way> {
 
   private void setValue(PreparedStatement statement, Way value)
       throws SQLException, JsonProcessingException {
-    statement.setObject(1, value.getId());
+    statement.setObject(1, value.id());
     statement.setObject(2, value.getInfo().getVersion());
     statement.setObject(3, value.getInfo().getUid());
     statement.setObject(4, value.getInfo().getTimestamp());
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/database/tile/PostgresTileStore.java b/baremaps-core/src/main/java/org/apache/baremaps/database/tile/PostgresTileStore.java
index 6e957f1c..be3b33a1 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/database/tile/PostgresTileStore.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/database/tile/PostgresTileStore.java
@@ -33,7 +33,7 @@ import net.sf.jsqlparser.expression.Parenthesis;
 import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
 import net.sf.jsqlparser.schema.Column;
 import net.sf.jsqlparser.statement.select.Join;
-import org.apache.baremaps.tileset.Tileset;
+import org.apache.baremaps.mvt.tileset.Tileset;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java b/baremaps-core/src/main/java/org/apache/baremaps/feature/Aggregate.java
similarity index 51%
copy from baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java
copy to baremaps-core/src/main/java/org/apache/baremaps/feature/Aggregate.java
index 5f4081ba..a13eb326 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/feature/Aggregate.java
@@ -10,29 +10,7 @@
  * the License.
  */
 
-package org.apache.baremaps.stream;
+package org.apache.baremaps.feature;
 
-
-
-import java.util.function.Consumer;
-import java.util.function.Function;
-
-/** Utility methods for dealing with consumers. */
-public class ConsumerUtils {
-
-  private ConsumerUtils() {}
-
-  /**
-   * Transforms a consumer into a function.
-   *
-   * @param consumer the consumer
-   * @param <T> the type
-   * @return the function
-   */
-  public static <T> Function<T, T> consumeThenReturn(Consumer<T> consumer) {
-    return t -> {
-      consumer.accept(t);
-      return t;
-    };
-  }
+public interface Aggregate extends Resource {
 }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java b/baremaps-core/src/main/java/org/apache/baremaps/feature/Feature.java
similarity index 52%
copy from baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java
copy to baremaps-core/src/main/java/org/apache/baremaps/feature/Feature.java
index 5f4081ba..93e6d8db 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/feature/Feature.java
@@ -10,29 +10,20 @@
  * the License.
  */
 
-package org.apache.baremaps.stream;
+package org.apache.baremaps.feature;
 
 
 
-import java.util.function.Consumer;
-import java.util.function.Function;
+import java.util.Map;
 
-/** Utility methods for dealing with consumers. */
-public class ConsumerUtils {
+public interface Feature {
 
-  private ConsumerUtils() {}
+  FeatureType getType();
+
+  void setProperty(String name, Object value);
+
+  Object getProperty(String name);
+
+  Map<String, Object> getProperties();
 
-  /**
-   * Transforms a consumer into a function.
-   *
-   * @param consumer the consumer
-   * @param <T> the type
-   * @return the function
-   */
-  public static <T> Function<T, T> consumeThenReturn(Consumer<T> consumer) {
-    return t -> {
-      consumer.accept(t);
-      return t;
-    };
-  }
 }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java b/baremaps-core/src/main/java/org/apache/baremaps/feature/FeatureCollection.java
similarity index 52%
copy from baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java
copy to baremaps-core/src/main/java/org/apache/baremaps/feature/FeatureCollection.java
index 5f4081ba..5a4d8eba 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/feature/FeatureCollection.java
@@ -10,29 +10,15 @@
  * the License.
  */
 
-package org.apache.baremaps.stream;
+package org.apache.baremaps.feature;
 
 
 
-import java.util.function.Consumer;
-import java.util.function.Function;
+import java.io.IOException;
+import java.util.Collection;
 
-/** Utility methods for dealing with consumers. */
-public class ConsumerUtils {
+public interface FeatureCollection extends Collection<Feature> {
 
-  private ConsumerUtils() {}
+  FeatureType getType() throws IOException;
 
-  /**
-   * Transforms a consumer into a function.
-   *
-   * @param consumer the consumer
-   * @param <T> the type
-   * @return the function
-   */
-  public static <T> Function<T, T> consumeThenReturn(Consumer<T> consumer) {
-    return t -> {
-      consumer.accept(t);
-      return t;
-    };
-  }
 }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/feature/FeatureImpl.java b/baremaps-core/src/main/java/org/apache/baremaps/feature/FeatureImpl.java
new file mode 100644
index 00000000..50f2605d
--- /dev/null
+++ b/baremaps-core/src/main/java/org/apache/baremaps/feature/FeatureImpl.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed 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.feature;
+
+
+
+import com.google.common.collect.ImmutableMap;
+import java.util.HashMap;
+import java.util.Map;
+
+public final class FeatureImpl implements Feature {
+
+  private final FeatureType type;
+
+  private final Map<String, Object> properties;
+
+  public FeatureImpl(FeatureType type) {
+    this(type, new HashMap<>());
+  }
+
+  public FeatureImpl(FeatureType type, Map<String, Object> properties) {
+    this.type = type;
+    this.properties = properties;
+  }
+
+  @Override
+  public FeatureType getType() {
+    return type;
+  }
+
+  @Override
+  public void setProperty(String name, Object value) {
+    properties.put(name, value);
+  }
+
+  @Override
+  public Object getProperty(String name) {
+    return properties.get(name);
+  }
+
+  public Map<String, Object> getProperties() {
+    return ImmutableMap.copyOf(properties);
+  }
+
+}
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java b/baremaps-core/src/main/java/org/apache/baremaps/feature/FeatureSet.java
similarity index 52%
copy from baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java
copy to baremaps-core/src/main/java/org/apache/baremaps/feature/FeatureSet.java
index 5f4081ba..2dcc4534 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/feature/FeatureSet.java
@@ -10,29 +10,14 @@
  * the License.
  */
 
-package org.apache.baremaps.stream;
+package org.apache.baremaps.feature;
 
 
 
-import java.util.function.Consumer;
-import java.util.function.Function;
+import java.io.IOException;
 
-/** Utility methods for dealing with consumers. */
-public class ConsumerUtils {
+public interface FeatureSet extends Resource {
 
-  private ConsumerUtils() {}
+  FeatureType getType() throws IOException;
 
-  /**
-   * Transforms a consumer into a function.
-   *
-   * @param consumer the consumer
-   * @param <T> the type
-   * @return the function
-   */
-  public static <T> Function<T, T> consumeThenReturn(Consumer<T> consumer) {
-    return t -> {
-      consumer.accept(t);
-      return t;
-    };
-  }
 }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/feature/FeatureType.java b/baremaps-core/src/main/java/org/apache/baremaps/feature/FeatureType.java
new file mode 100644
index 00000000..3829b16a
--- /dev/null
+++ b/baremaps-core/src/main/java/org/apache/baremaps/feature/FeatureType.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed 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.feature;
+
+
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+
+public class FeatureType {
+
+  private final String name;
+
+  private final Map<String, PropertyType> properties;
+
+  public FeatureType(String name, Map<String, PropertyType> properties) {
+    this.name = name;
+    this.properties = properties;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public Map<String, PropertyType> getProperties() {
+    return ImmutableMap.copyOf(properties);
+  }
+
+  public PropertyType getProperty(String name) {
+    return properties.get(name);
+  }
+
+  public Feature newInstance() {
+    return new FeatureImpl(this);
+  }
+
+}
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java b/baremaps-core/src/main/java/org/apache/baremaps/feature/PropertyType.java
similarity index 52%
copy from baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java
copy to baremaps-core/src/main/java/org/apache/baremaps/feature/PropertyType.java
index 5f4081ba..634657c4 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/feature/PropertyType.java
@@ -10,29 +10,24 @@
  * the License.
  */
 
-package org.apache.baremaps.stream;
+package org.apache.baremaps.feature;
 
+public class PropertyType<T> {
 
+  private final String name;
+  private final Class<T> type;
 
-import java.util.function.Consumer;
-import java.util.function.Function;
-
-/** Utility methods for dealing with consumers. */
-public class ConsumerUtils {
+  public PropertyType(String name, Class<T> type) {
+    this.name = name;
+    this.type = type;
+  }
 
-  private ConsumerUtils() {}
+  public String getName() {
+    return name;
+  }
 
-  /**
-   * Transforms a consumer into a function.
-   *
-   * @param consumer the consumer
-   * @param <T> the type
-   * @return the function
-   */
-  public static <T> Function<T, T> consumeThenReturn(Consumer<T> consumer) {
-    return t -> {
-      consumer.accept(t);
-      return t;
-    };
+  public Class<T> getType() {
+    return type;
   }
+
 }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java b/baremaps-core/src/main/java/org/apache/baremaps/feature/ReadableAggregate.java
similarity index 52%
copy from baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java
copy to baremaps-core/src/main/java/org/apache/baremaps/feature/ReadableAggregate.java
index 5f4081ba..2493c211 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/feature/ReadableAggregate.java
@@ -10,29 +10,15 @@
  * the License.
  */
 
-package org.apache.baremaps.stream;
+package org.apache.baremaps.feature;
 
 
 
-import java.util.function.Consumer;
-import java.util.function.Function;
+import java.io.IOException;
+import java.util.stream.Stream;
 
-/** Utility methods for dealing with consumers. */
-public class ConsumerUtils {
+public interface ReadableAggregate extends Aggregate {
 
-  private ConsumerUtils() {}
+  Stream<Resource> read() throws IOException;
 
-  /**
-   * Transforms a consumer into a function.
-   *
-   * @param consumer the consumer
-   * @param <T> the type
-   * @return the function
-   */
-  public static <T> Function<T, T> consumeThenReturn(Consumer<T> consumer) {
-    return t -> {
-      consumer.accept(t);
-      return t;
-    };
-  }
 }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java b/baremaps-core/src/main/java/org/apache/baremaps/feature/ReadableFeatureSet.java
similarity index 52%
copy from baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java
copy to baremaps-core/src/main/java/org/apache/baremaps/feature/ReadableFeatureSet.java
index 5f4081ba..93547816 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/feature/ReadableFeatureSet.java
@@ -10,29 +10,15 @@
  * the License.
  */
 
-package org.apache.baremaps.stream;
+package org.apache.baremaps.feature;
 
 
 
-import java.util.function.Consumer;
-import java.util.function.Function;
+import java.io.IOException;
+import java.util.stream.Stream;
 
-/** Utility methods for dealing with consumers. */
-public class ConsumerUtils {
+public interface ReadableFeatureSet extends FeatureSet {
 
-  private ConsumerUtils() {}
+  Stream<Feature> read() throws IOException;
 
-  /**
-   * Transforms a consumer into a function.
-   *
-   * @param consumer the consumer
-   * @param <T> the type
-   * @return the function
-   */
-  public static <T> Function<T, T> consumeThenReturn(Consumer<T> consumer) {
-    return t -> {
-      consumer.accept(t);
-      return t;
-    };
-  }
 }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/feature/ReflectiveFeatureAdapter.java b/baremaps-core/src/main/java/org/apache/baremaps/feature/ReflectiveFeatureAdapter.java
new file mode 100644
index 00000000..dc26c1e6
--- /dev/null
+++ b/baremaps-core/src/main/java/org/apache/baremaps/feature/ReflectiveFeatureAdapter.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed 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.feature;
+
+
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ReflectiveFeatureAdapter implements Feature {
+
+  private final Object feature;
+
+  public ReflectiveFeatureAdapter(Object feature) {
+    this.feature = feature;
+  }
+
+  @Override
+  public FeatureType getType() {
+    var type = feature.getClass();
+    var name = type.getSimpleName();
+    var fields = type.getDeclaredFields();
+    var properties = new HashMap<String, PropertyType>();
+    for (Field field : fields) {
+      field.setAccessible(true);
+      var propertyName = field.getName();
+      var propertyType = new PropertyType(propertyName, field.getType());
+      properties.put(propertyName, propertyType);
+    }
+    return new FeatureType(name, properties);
+  }
+
+  @Override
+  public Object getProperty(String name) {
+    try {
+      var field = this.getClass().getDeclaredField(name);
+      field.setAccessible(true);
+      return field.get(this);
+    } catch (IllegalAccessException e) {
+      throw new IllegalArgumentException(e);
+    } catch (NoSuchFieldException e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
+
+  @Override
+  public void setProperty(String name, Object value) throws IllegalArgumentException {
+    try {
+      var field = this.getClass().getDeclaredField(name);
+      field.setAccessible(true);
+      field.set(this, value);
+    } catch (IllegalAccessException e) {
+      throw new IllegalArgumentException(e);
+    } catch (NoSuchFieldException e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
+
+
+  @Override
+  public Map<String, Object> getProperties() {
+    var type = feature.getClass();
+    var fields = type.getDeclaredFields();
+    var properties = new HashMap<String, Object>();
+    for (Field field : fields) {
+      field.setAccessible(true);
+      var propertyName = field.getName();
+      try {
+        var propertyValue = field.get(this);
+        properties.put(propertyName, propertyValue);
+      } catch (IllegalAccessException e) {
+        // Ignore the field
+      }
+    }
+    return properties;
+  }
+
+
+}
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java b/baremaps-core/src/main/java/org/apache/baremaps/feature/Resource.java
similarity index 52%
copy from baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java
copy to baremaps-core/src/main/java/org/apache/baremaps/feature/Resource.java
index 5f4081ba..a9686585 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/feature/Resource.java
@@ -10,29 +10,10 @@
  * the License.
  */
 
-package org.apache.baremaps.stream;
+package org.apache.baremaps.feature;
 
 
 
-import java.util.function.Consumer;
-import java.util.function.Function;
+public interface Resource {
 
-/** Utility methods for dealing with consumers. */
-public class ConsumerUtils {
-
-  private ConsumerUtils() {}
-
-  /**
-   * Transforms a consumer into a function.
-   *
-   * @param consumer the consumer
-   * @param <T> the type
-   * @return the function
-   */
-  public static <T> Function<T, T> consumeThenReturn(Consumer<T> consumer) {
-    return t -> {
-      consumer.accept(t);
-      return t;
-    };
-  }
 }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java b/baremaps-core/src/main/java/org/apache/baremaps/feature/WritableAggregate.java
similarity index 52%
copy from baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java
copy to baremaps-core/src/main/java/org/apache/baremaps/feature/WritableAggregate.java
index 5f4081ba..87d300c4 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/feature/WritableAggregate.java
@@ -10,29 +10,16 @@
  * the License.
  */
 
-package org.apache.baremaps.stream;
+package org.apache.baremaps.feature;
 
 
 
-import java.util.function.Consumer;
-import java.util.function.Function;
+import java.io.IOException;
 
-/** Utility methods for dealing with consumers. */
-public class ConsumerUtils {
+public interface WritableAggregate extends Aggregate {
 
-  private ConsumerUtils() {}
+  void write(Resource resource) throws IOException;
+
+  void remove(Resource resource) throws IOException;
 
-  /**
-   * Transforms a consumer into a function.
-   *
-   * @param consumer the consumer
-   * @param <T> the type
-   * @return the function
-   */
-  public static <T> Function<T, T> consumeThenReturn(Consumer<T> consumer) {
-    return t -> {
-      consumer.accept(t);
-      return t;
-    };
-  }
 }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java b/baremaps-core/src/main/java/org/apache/baremaps/feature/WritableFeatureSet.java
similarity index 52%
copy from baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java
copy to baremaps-core/src/main/java/org/apache/baremaps/feature/WritableFeatureSet.java
index 5f4081ba..a7541ef4 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/feature/WritableFeatureSet.java
@@ -10,29 +10,15 @@
  * the License.
  */
 
-package org.apache.baremaps.stream;
+package org.apache.baremaps.feature;
 
 
 
-import java.util.function.Consumer;
-import java.util.function.Function;
+import java.io.IOException;
+import java.util.stream.Stream;
 
-/** Utility methods for dealing with consumers. */
-public class ConsumerUtils {
+public interface WritableFeatureSet extends FeatureSet {
 
-  private ConsumerUtils() {}
+  void write(Stream<Feature> features) throws IOException;
 
-  /**
-   * Transforms a consumer into a function.
-   *
-   * @param consumer the consumer
-   * @param <T> the type
-   * @return the function
-   */
-  public static <T> Function<T, T> consumeThenReturn(Consumer<T> consumer) {
-    return t -> {
-      consumer.accept(t);
-      return t;
-    };
-  }
 }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/mvt/Expressions.java b/baremaps-core/src/main/java/org/apache/baremaps/mvt/Expressions.java
new file mode 100644
index 00000000..7ff03fc9
--- /dev/null
+++ b/baremaps-core/src/main/java/org/apache/baremaps/mvt/Expressions.java
@@ -0,0 +1,469 @@
+/*
+ * Licensed 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.mvt;
+
+
+
+import com.fasterxml.jackson.core.JacksonException;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+import java.io.IOException;
+import java.io.StringReader;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Predicate;
+import org.apache.baremaps.feature.Feature;
+
+public interface Expressions {
+
+  interface Expression<T> {
+
+    String name();
+
+    T evaluate(Feature feature);
+
+  }
+
+  record Literal(Object value) implements Expression {
+
+  @Override
+        public String name() {
+            return "literal";
+        }
+
+  @Override
+        public Object evaluate(Feature feature) {
+            return value;
+        }}
+
+  record At(int index, Expression expression) implements Expression {
+
+  @Override
+        public String name() {
+            return "at";
+        }
+
+  @Override
+        public Object evaluate(Feature feature) {
+            Object value = expression.evaluate(feature);
+            if (value instanceof List list && index >= 0 && index < list.size()) {
+                return list.get(index);
+            }
+            return null;
+        }}
+
+  record Get(String property) implements Expression {
+
+  @Override
+        public String name() {
+            return "get";
+        }
+
+  @Override
+        public Object evaluate(Feature feature) {
+            return feature.getProperty(property);
+        }}
+
+  record Has(String property) implements Expression<Boolean> {
+
+  @Override
+        public String name() {
+            return "has";
+        }
+
+  @Override
+        public Boolean evaluate(Feature feature) {
+            return feature.getProperty(property) != null;
+        }}
+
+  record In(Object value, Expression expression) implements Expression<Boolean> {
+
+  @Override
+        public String name() {
+            return "in";
+        }
+
+  @Override
+        public Boolean evaluate(Feature feature) {
+            var expressionValue = expression.evaluate(feature);
+            if (expressionValue instanceof List list) {
+                return list.contains(value);
+            } else if (expressionValue instanceof String string) {
+                return string.contains(value.toString());
+            } else {
+                return false;
+            }
+        }}
+
+  record IndexOf(Object value, Expression expression) implements Expression<Integer> {
+
+  @Override
+        public String name() {
+            return "index-of";
+        }
+
+  @Override
+        public Integer evaluate(Feature feature) {
+            var expressionValue = expression.evaluate(feature);
+            if (expressionValue instanceof List list) {
+                return list.indexOf(value);
+            } else if (expressionValue instanceof String string) {
+                return string.indexOf(value.toString());
+            } else {
+                return -1;
+            }
+        }}
+
+  record Length(Expression expression) implements Expression<Integer> {
+
+  @Override
+        public String name() {
+            return "length";
+        }
+
+  @Override
+        public Integer evaluate(Feature feature) {
+            Object value = expression.evaluate(feature);
+            if (value instanceof String string) {
+                return string.length();
+            } else if (value instanceof List list) {
+                return list.size();
+            } else {
+                return -1;
+            }
+        }}
+
+  record Slice(Expression expression, Expression start, Expression end) implements Expression {
+
+  public Slice(Expression expression, Expression start) {
+            this(expression, start, null);
+        }
+
+  @Override
+        public String name() {
+            return "slice";
+        }
+
+  @Override
+        public Object evaluate(Feature feature) {
+            Object value = expression.evaluate(feature);
+            var startIndex = (Integer) start.evaluate(feature);
+            if (value instanceof String string) {
+                var endIndex = end == null ? string.length() : (Integer) end.evaluate(feature);
+                return string.substring(startIndex, endIndex);
+            } else if (value instanceof List list) {
+                var endIndex = end == null ? list.size() : (Integer) end.evaluate(feature);
+                return list.subList(startIndex, endIndex);
+            } else {
+                return List.of();
+            }
+        }}
+
+  record Not(Expression expression) implements Expression {
+
+  @Override
+        public String name() {
+            return "!";
+        }
+
+  @Override
+        public Object evaluate(Feature feature) {
+            return !(boolean) expression.evaluate(feature);
+        }}
+
+  record NotEqual(Expression left, Expression right) implements Expression {
+
+  @Override
+        public String name() {
+            return "!=";
+        }
+
+  @Override
+        public Object evaluate(Feature feature) {
+            return new Not(new Equal(left, right)).evaluate(feature);
+        }}
+
+  record Less(Expression left, Expression right) implements Expression<Boolean> {
+
+  @Override
+        public String name() {
+            return "<";
+        }
+
+  @Override
+        public Boolean evaluate(Feature feature) {
+            return (double) left.evaluate(feature) < (double) right.evaluate(feature);
+        }}
+
+  record LessOrEqual(Expression left, Expression right) implements Expression {
+
+  @Override
+        public String name() {
+            return "<=";
+        }
+
+  @Override
+        public Object evaluate(Feature feature) {
+            return (double) left.evaluate(feature) <= (double) right.evaluate(feature);
+        }}
+
+  record Equal(Expression left, Expression right) implements Expression {
+
+  @Override
+        public String name() {
+            return "==";
+        }
+
+  @Override
+        public Object evaluate(Feature feature) {
+            return left.evaluate(feature).equals(right.evaluate(feature));
+        }}
+
+  record Greater(Expression left, Expression right) implements Expression<Boolean> {
+
+  @Override
+        public String name() {
+            return ">";
+        }
+
+  @Override
+        public Boolean evaluate(Feature feature) {
+            return (double) left.evaluate(feature) > (double) right.evaluate(feature);
+        }}
+
+  record GreaterOrEqual(Expression left, Expression right) implements Expression<Boolean> {
+
+  @Override
+        public String name() {
+            return ">=";
+        }
+
+  @Override
+        public Boolean evaluate(Feature feature) {
+            return (double) left.evaluate(feature) >= (double) right.evaluate(feature);
+        }}
+
+  record All(List<Expression> expressions) implements Expression {
+
+  @Override
+        public String name() {
+            return "all";
+        }
+
+  @Override
+        public Object evaluate(Feature feature) {
+            return expressions.stream().allMatch(expression -> (boolean) expression.evaluate(feature));
+        }}
+
+  record Any(List<Expression> expressions) implements Expression {
+
+  @Override
+        public String name() {
+            return "any";
+        }
+
+  @Override
+        public Object evaluate(Feature feature) {
+            return expressions.stream().anyMatch(expression -> (boolean) expression.evaluate(feature));
+        }}
+
+  record Case(Expression condition, Expression then, Expression otherwise) implements Expression {
+
+  @Override
+        public String name() {
+            return "case";
+        }
+
+  @Override
+        public Object evaluate(Feature feature) {
+            if ((boolean) condition.evaluate(feature)) {
+                return then.evaluate(feature);
+            } else {
+                return otherwise.evaluate(feature);
+            }
+        }}
+
+  record Coalesce(List<Expression> expressions) implements Expression {
+
+  @Override
+        public String name() {
+            return "coalesce";
+        }
+
+  @Override
+        public Object evaluate(Feature feature) {
+            for (Expression expression : expressions) {
+                Object value = expression.evaluate(feature);
+                if (value != null) {
+                    return value;
+                }
+            }
+            return null;
+        }}
+
+  record Match(Expression input, List<Expression> cases, Expression fallback) implements Expression {
+
+  @Override
+        public String name() {
+            return "match";
+        }
+
+  @Override
+        public Object evaluate(Feature feature) {
+            if (cases.size() % 2 != 0) {
+                throw new IllegalArgumentException("match expression must have an even number of arguments");
+            }
+            var inputValue = input.evaluate(feature);
+            for (int i = 0; i < cases.size(); i += 2) {
+                Expression condition = cases.get(i);
+                Expression then = cases.get(i + 1);
+                if (inputValue.equals(condition.evaluate(feature))) {
+                    return then.evaluate(feature);
+                }
+            }
+            return fallback.evaluate(feature);
+        }}
+
+  record Within(Expression expression) implements Expression {
+
+  @Override
+        public String name() {
+            return "within";
+        }
+
+  @Override
+        public Object evaluate(Feature feature) {
+            throw new UnsupportedOperationException("within expression is not supported");
+        }
+}
+
+
+class ExpressionSerializer extends StdSerializer<Expression> {
+
+  public ExpressionSerializer() {
+    super(Expression.class);
+  }
+
+  @Override
+  public void serialize(Expression expression, JsonGenerator jsonGenerator,
+      SerializerProvider serializerProvider) throws IOException {
+    jsonGenerator.writeStartArray();
+    jsonGenerator.writeString(expression.name());
+    for (Field field : expression.getClass().getDeclaredFields()) {
+      field.setAccessible(true);
+      try {
+        Object value = field.get(expression);
+        if (value instanceof Expression subExpression) {
+          serialize(subExpression, jsonGenerator, serializerProvider);
+        } else {
+          jsonGenerator.writeObject(value);
+        }
+      } catch (IllegalAccessException e) {
+        throw new RuntimeException(e);
+      }
+    }
+    jsonGenerator.writeEndArray();
+  }
+}
+
+
+class ExpressionDeserializer extends StdDeserializer<Expression> {
+
+  protected ExpressionDeserializer() {
+    super(Expression.class);
+  }
+
+  @Override
+  public Expression deserialize(JsonParser jsonParser,
+      DeserializationContext deserializationContext) throws IOException, JacksonException {
+    JsonNode node = jsonParser.getCodec().readTree(jsonParser);
+    return deserializeJsonNode(node);
+  }
+
+  public Expression deserializeJsonNode(JsonNode node) {
+            return switch (node.getNodeType()) {
+                case BOOLEAN -> new Literal(node.asBoolean());
+                case NUMBER -> new Literal(node.asDouble());
+                case STRING -> new Literal(node.asText());
+                case ARRAY -> deserializeJsonArray(node);
+                default -> throw new IllegalArgumentException("Unknown node type: " + node.getNodeType());
+            };
+        }
+
+        public Expression deserializeJsonArray(JsonNode node) {
+            var arrayList = new ArrayList<JsonNode>();
+            node.elements().forEachRemaining(arrayList::add);
+            return switch (arrayList.get(0).asText()) {
+                case "literal" -> new Literal(arrayList.get(1).asText());
+                case "get" -> new Get(arrayList.get(1).asText());
+                case "has" -> new Has(arrayList.get(1).asText());
+                case ">" -> new Greater(deserializeJsonNode(arrayList.get(1)), deserializeJsonNode(arrayList.get(2)));
+                case ">=" ->
+                        new GreaterOrEqual(deserializeJsonNode(arrayList.get(1)), deserializeJsonNode(arrayList.get(2)));
+                case "<" -> new Less(deserializeJsonNode(arrayList.get(1)), deserializeJsonNode(arrayList.get(2)));
+                case "<=" ->
+                        new LessOrEqual(deserializeJsonNode(arrayList.get(1)), deserializeJsonNode(arrayList.get(2)));
+                case "==" -> new Equal(deserializeJsonNode(arrayList.get(1)), deserializeJsonNode(arrayList.get(2)));
+                case "!=" -> new NotEqual(deserializeJsonNode(arrayList.get(1)), deserializeJsonNode(arrayList.get(2)));
+                case "!" -> new Not(deserializeJsonNode(arrayList.get(1)));
+                case "all" -> new All(arrayList.stream().skip(1).map(this::deserializeJsonNode).toList());
+                case "any" -> new Any(arrayList.stream().skip(1).map(this::deserializeJsonNode).toList());
+                case "case" ->
+                        new Case(deserializeJsonNode(arrayList.get(1)), deserializeJsonNode(arrayList.get(2)), deserializeJsonNode(arrayList.get(3)));
+                case "coalesce" -> new Coalesce(arrayList.stream().skip(1).map(this::deserializeJsonNode).toList());
+                case "match" ->
+                        new Match(deserializeJsonNode(arrayList.get(1)), arrayList.subList(2, arrayList.size() - 1).stream().map(this::deserializeJsonNode).toList(), deserializeJsonNode(arrayList.get(arrayList.size() - 1)));
+                case "within" -> new Within(deserializeJsonNode(arrayList.get(1)));
+                default -> throw new IllegalArgumentException("Unknown expression: " + arrayList.get(0).asText());
+            };
+        }
+    }
+
+
+    static Expression read(String json) throws IOException {
+        var mapper = new ObjectMapper();
+        var simpleModule = new SimpleModule("SimpleModule", new Version(1, 0, 0, null));
+        simpleModule.addDeserializer(Expression.class, new ExpressionDeserializer());
+        mapper.registerModule(simpleModule);
+        return mapper.readValue(new StringReader(json), Expression.class);
+    }
+
+    static String write(Expression expression) throws IOException {
+        var mapper = new ObjectMapper();
+        var simpleModule = new SimpleModule("SimpleModule", new Version(1, 0, 0, null));
+        simpleModule.addSerializer(Expression.class, new ExpressionSerializer());
+        mapper.registerModule(simpleModule);
+        return mapper.writeValueAsString(expression);
+    }
+
+    static Predicate<Feature> asPredicate(Expression expression) {
+        return feature -> {
+            var result = expression.evaluate(feature);
+            if (result instanceof Boolean booleanResult) {
+                return booleanResult;
+            }
+            throw new IllegalArgumentException("Expression does not evaluate to a boolean: " + expression);
+        };
+    }
+
+
+}
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/style/Style.java b/baremaps-core/src/main/java/org/apache/baremaps/mvt/style/Style.java
similarity index 99%
rename from baremaps-core/src/main/java/org/apache/baremaps/style/Style.java
rename to baremaps-core/src/main/java/org/apache/baremaps/mvt/style/Style.java
index 8a5a8dd5..e26a7556 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/style/Style.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/mvt/style/Style.java
@@ -10,7 +10,7 @@
  * the License.
  */
 
-package org.apache.baremaps.style;
+package org.apache.baremaps.mvt.style;
 
 
 
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/style/StyleLayer.java b/baremaps-core/src/main/java/org/apache/baremaps/mvt/style/StyleLayer.java
similarity index 99%
rename from baremaps-core/src/main/java/org/apache/baremaps/style/StyleLayer.java
rename to baremaps-core/src/main/java/org/apache/baremaps/mvt/style/StyleLayer.java
index 174a61a1..c4218337 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/style/StyleLayer.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/mvt/style/StyleLayer.java
@@ -10,7 +10,7 @@
  * the License.
  */
 
-package org.apache.baremaps.style;
+package org.apache.baremaps.mvt.style;
 
 
 
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/style/StyleSource.java b/baremaps-core/src/main/java/org/apache/baremaps/mvt/style/StyleSource.java
similarity index 98%
rename from baremaps-core/src/main/java/org/apache/baremaps/style/StyleSource.java
rename to baremaps-core/src/main/java/org/apache/baremaps/mvt/style/StyleSource.java
index c9709d29..90ea439b 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/style/StyleSource.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/mvt/style/StyleSource.java
@@ -10,7 +10,7 @@
  * the License.
  */
 
-package org.apache.baremaps.style;
+package org.apache.baremaps.mvt.style;
 
 
 
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/tileset/Tileset.java b/baremaps-core/src/main/java/org/apache/baremaps/mvt/tileset/Tileset.java
similarity index 99%
rename from baremaps-core/src/main/java/org/apache/baremaps/tileset/Tileset.java
rename to baremaps-core/src/main/java/org/apache/baremaps/mvt/tileset/Tileset.java
index add7861c..b47a58cc 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/tileset/Tileset.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/mvt/tileset/Tileset.java
@@ -10,7 +10,7 @@
  * the License.
  */
 
-package org.apache.baremaps.tileset;
+package org.apache.baremaps.mvt.tileset;
 
 
 
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/tileset/TilesetLayer.java b/baremaps-core/src/main/java/org/apache/baremaps/mvt/tileset/TilesetLayer.java
similarity index 98%
rename from baremaps-core/src/main/java/org/apache/baremaps/tileset/TilesetLayer.java
rename to baremaps-core/src/main/java/org/apache/baremaps/mvt/tileset/TilesetLayer.java
index 4c2d3338..7160e3a5 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/tileset/TilesetLayer.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/mvt/tileset/TilesetLayer.java
@@ -10,7 +10,7 @@
  * the License.
  */
 
-package org.apache.baremaps.tileset;
+package org.apache.baremaps.mvt.tileset;
 
 
 
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/tileset/TilesetQuery.java b/baremaps-core/src/main/java/org/apache/baremaps/mvt/tileset/TilesetQuery.java
similarity index 97%
rename from baremaps-core/src/main/java/org/apache/baremaps/tileset/TilesetQuery.java
rename to baremaps-core/src/main/java/org/apache/baremaps/mvt/tileset/TilesetQuery.java
index 95b9da78..18ff7476 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/tileset/TilesetQuery.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/mvt/tileset/TilesetQuery.java
@@ -10,7 +10,7 @@
  * the License.
  */
 
-package org.apache.baremaps.tileset;
+package org.apache.baremaps.mvt.tileset;
 
 
 
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/function/CoordinateMapBuilder.java b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/function/CoordinateMapBuilder.java
index b0e1492b..9f0d6983 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/function/CoordinateMapBuilder.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/function/CoordinateMapBuilder.java
@@ -38,7 +38,7 @@ public class CoordinateMapBuilder implements Consumer<Entity> {
   @Override
   public void accept(Entity entity) {
     if (entity instanceof Node node) {
-      coordinateMap.put(node.getId(), new Coordinate(node.getLon(), node.getLat()));
+      coordinateMap.put(node.id(), new Coordinate(node.getLon(), node.getLat()));
     }
   }
 }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/function/GeometryMapBuilder.java b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/function/GeometryMapBuilder.java
index 2828b496..3ee34bcf 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/function/GeometryMapBuilder.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/function/GeometryMapBuilder.java
@@ -45,7 +45,7 @@ public class GeometryMapBuilder implements Consumer<Entity> {
   @Override
   public void accept(Entity entity) {
     if (filter.test(entity) && entity instanceof Element element) {
-      geometryMap.put(element.getId(), element.getGeometry());
+      geometryMap.put(element.id(), element.getGeometry());
     }
   }
 }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/function/ReferenceMapBuilder.java b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/function/ReferenceMapBuilder.java
index 6f720b1d..b389662d 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/function/ReferenceMapBuilder.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/function/ReferenceMapBuilder.java
@@ -38,7 +38,7 @@ public class ReferenceMapBuilder implements Consumer<Entity> {
   @Override
   public void accept(Entity entity) {
     if (entity instanceof Way way) {
-      referenceMap.put(way.getId(), way.getNodes());
+      referenceMap.put(way.id(), way.getNodes());
     }
   }
 }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/function/RelationGeometryBuilder.java b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/function/RelationGeometryBuilder.java
index 552837f3..d84d48ae 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/function/RelationGeometryBuilder.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/function/RelationGeometryBuilder.java
@@ -55,7 +55,7 @@ public class RelationGeometryBuilder implements Consumer<Relation> {
   @Override
   public void accept(Relation relation) {
     try {
-      Map<String, String> tags = relation.getTags();
+      Map<String, Object> tags = relation.getTags();
 
       // Filter multipolygon geometries
       if (!"multipolygon".equals(tags.get("type"))) {
@@ -87,7 +87,7 @@ public class RelationGeometryBuilder implements Consumer<Relation> {
         relation.setGeometry(multiPolygon);
       }
     } catch (Exception e) {
-      logger.warn("Unable to build the geometry for relation #" + relation.getId(), e);
+      logger.warn("Unable to build the geometry for relation #" + relation.id(), e);
     }
   }
 
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/function/WayGeometryBuilder.java b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/function/WayGeometryBuilder.java
index 3d6f21bc..32c76eb3 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/function/WayGeometryBuilder.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/function/WayGeometryBuilder.java
@@ -61,7 +61,7 @@ public class WayGeometryBuilder implements Consumer<Way> {
         }
       }
     } catch (Exception e) {
-      logger.warn("Unable to build the geometry for way #" + way.getId(), e);
+      logger.warn("Unable to build the geometry for way #" + way.id(), e);
     }
   }
 }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/model/Element.java b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/model/Element.java
index 52200f87..80c2136a 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/model/Element.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/model/Element.java
@@ -33,11 +33,11 @@ permits Node, Way, Relation
 
   protected final Info info;
 
-  protected final Map<String, String> tags;
+  protected final Map<String, Object> tags;
 
   protected Geometry geometry;
 
-  protected Element(long id, Info info, Map<String, String> tags) {
+  protected Element(long id, Info info, Map<String, Object> tags) {
     this(id, info, tags, null);
   }
 
@@ -49,7 +49,7 @@ permits Node, Way, Relation
    * @param tags the tags
    * @param geometry the geometry
    */
-  protected Element(long id, Info info, Map<String, String> tags, Geometry geometry) {
+  protected Element(long id, Info info, Map<String, Object> tags, Geometry geometry) {
     this.id = id;
     this.info = info;
     this.tags = tags;
@@ -61,7 +61,7 @@ permits Node, Way, Relation
    *
    * @return the id
    */
-  public long getId() {
+  public long id() {
     return id;
   }
 
@@ -79,7 +79,7 @@ permits Node, Way, Relation
    *
    * @return the tags
    */
-  public Map<String, String> getTags() {
+  public Map<String, Object> getTags() {
     return tags;
   }
 
@@ -127,4 +127,5 @@ permits Node, Way, Relation
     return new StringJoiner(", ", Element.class.getSimpleName() + "[", "]").add("id=" + id)
         .add("info=" + info).add("tags=" + tags).add("geometry=" + geometry).toString();
   }
+
 }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/model/Node.java b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/model/Node.java
index 7444bac4..a7d76e09 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/model/Node.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/model/Node.java
@@ -35,7 +35,7 @@ public final class Node extends Element {
    * @param lon the longitude
    * @param lat the latitude
    */
-  public Node(long id, Info info, Map<String, String> tags, double lon, double lat) {
+  public Node(long id, Info info, Map<String, Object> tags, double lon, double lat) {
     super(id, info, tags);
     this.lon = lon;
     this.lat = lat;
@@ -51,7 +51,7 @@ public final class Node extends Element {
    * @param lat the latitude
    * @param geometry the geometry
    */
-  public Node(long id, Info info, Map<String, String> tags, double lon, double lat,
+  public Node(long id, Info info, Map<String, Object> tags, double lon, double lat,
       Geometry geometry) {
     super(id, info, tags, geometry);
     this.lon = lon;
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/model/Relation.java b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/model/Relation.java
index 653f2a1d..168fb0d1 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/model/Relation.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/model/Relation.java
@@ -33,7 +33,7 @@ public final class Relation extends Element {
    * @param tags the tags
    * @param members the members
    */
-  public Relation(long id, Info info, Map<String, String> tags, List<Member> members) {
+  public Relation(long id, Info info, Map<String, Object> tags, List<Member> members) {
     super(id, info, tags);
     this.members = members;
   }
@@ -47,7 +47,7 @@ public final class Relation extends Element {
    * @param members the members
    * @param geometry the geometry
    */
-  public Relation(long id, Info info, Map<String, String> tags, List<Member> members,
+  public Relation(long id, Info info, Map<String, Object> tags, List<Member> members,
       Geometry geometry) {
     super(id, info, tags, geometry);
     this.members = members;
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/model/Way.java b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/model/Way.java
index 5513b1d5..d1ed67ab 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/model/Way.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/model/Way.java
@@ -33,7 +33,7 @@ public final class Way extends Element {
    * @param tags the tags
    * @param nodes the nodes
    */
-  public Way(long id, Info info, Map<String, String> tags, List<Long> nodes) {
+  public Way(long id, Info info, Map<String, Object> tags, List<Long> nodes) {
     super(id, info, tags);
     this.nodes = nodes;
   }
@@ -47,7 +47,7 @@ public final class Way extends Element {
    * @param nodes the nodes
    * @param geometry the geometry
    */
-  public Way(long id, Info info, Map<String, String> tags, List<Long> nodes, Geometry geometry) {
+  public Way(long id, Info info, Map<String, Object> tags, List<Long> nodes, Geometry geometry) {
     super(id, info, tags, geometry);
     this.nodes = nodes;
   }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/pbf/DataBlockReader.java b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/pbf/DataBlockReader.java
index 3b6f92b1..a6c8e896 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/pbf/DataBlockReader.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/pbf/DataBlockReader.java
@@ -130,7 +130,7 @@ public class DataBlockReader {
         lon = denseNodes.getLon(i) + lon;
 
         // If empty, assume that nothing here has keys or vals.
-        Map<String, String> tags = new HashMap<>();
+        Map<String, Object> tags = new HashMap<>();
         if (denseNodes.getKeysValsCount() > 0) {
           while (denseNodes.getKeysVals(j) != 0) {
             int keyid = denseNodes.getKeysVals(j++);
@@ -159,7 +159,7 @@ public class DataBlockReader {
         LocalDateTime timestamp = getTimestamp(node.getInfo().getTimestamp());
         long changeset = node.getInfo().getChangeset();
         int uid = node.getInfo().getUid();
-        Map<String, String> tags = new HashMap<>();
+        Map<String, Object> tags = new HashMap<>();
         for (int t = 0; t < node.getKeysList().size(); t++) {
           tags.put(getString(node.getKeysList().get(t)), getString(node.getKeysList().get(t)));
         }
@@ -185,7 +185,7 @@ public class DataBlockReader {
         LocalDateTime timestamp = getTimestamp(way.getInfo().getTimestamp());
         long changeset = way.getInfo().getChangeset();
         int uid = way.getInfo().getUid();
-        Map<String, String> tags = getTags(way.getKeysList(), way.getValsList());
+        Map<String, Object> tags = getTags(way.getKeysList(), way.getValsList());
         long nid = 0;
         List<Long> nodes = new ArrayList<>();
         for (int index = 0; index < way.getRefsCount(); index++) {
@@ -212,7 +212,7 @@ public class DataBlockReader {
         LocalDateTime timestamp = getTimestamp(relation.getInfo().getTimestamp());
         long changeset = relation.getInfo().getChangeset();
         int uid = relation.getInfo().getUid();
-        Map<String, String> tags = getTags(relation.getKeysList(), relation.getValsList());
+        Map<String, Object> tags = getTags(relation.getKeysList(), relation.getValsList());
 
         long mid = 0;
         List<Member> members = new ArrayList<>();
@@ -255,8 +255,8 @@ public class DataBlockReader {
         TimeZone.getDefault().toZoneId());
   }
 
-  private Map<String, String> getTags(List<Integer> keys, List<Integer> vals) {
-    Map<String, String> tags = new HashMap<>();
+  private Map<String, Object> getTags(List<Integer> keys, List<Integer> vals) {
+    Map<String, Object> tags = new HashMap<>();
     for (int t = 0; t < keys.size(); t++) {
       tags.put(getString(keys.get(t)), getString(vals.get(t)));
     }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/xml/XmlChangeSpliterator.java b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/xml/XmlChangeSpliterator.java
index 83080d64..26c8536d 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/xml/XmlChangeSpliterator.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/xml/XmlChangeSpliterator.java
@@ -154,7 +154,7 @@ public class XmlChangeSpliterator implements Spliterator<Change> {
     double longitude = Double.parseDouble(reader.getAttributeValue(null, ATTRIBUTE_NAME_LONGITUDE));
 
     // read the content of the node
-    Map<String, String> tags = new HashMap<>();
+    Map<String, Object> tags = new HashMap<>();
     reader.nextTag();
     while (reader.getEventType() == XMLStreamConstants.START_ELEMENT) {
       switch (reader.getLocalName()) {
@@ -175,7 +175,7 @@ public class XmlChangeSpliterator implements Spliterator<Change> {
     Info info = readInfo();
 
     // read the content of the node
-    Map<String, String> tags = new HashMap<>();
+    Map<String, Object> tags = new HashMap<>();
     List<Long> members = new ArrayList<>();
     reader.nextTag();
     while (reader.getEventType() == XMLStreamConstants.START_ELEMENT) {
@@ -207,7 +207,7 @@ public class XmlChangeSpliterator implements Spliterator<Change> {
     Info info = readInfo();
 
     // read the content of the node
-    Map<String, String> tags = new HashMap<>();
+    Map<String, Object> tags = new HashMap<>();
     List<Member> members = new ArrayList<>();
     reader.nextTag();
     while (reader.getEventType() == XMLStreamConstants.START_ELEMENT) {
@@ -248,7 +248,7 @@ public class XmlChangeSpliterator implements Spliterator<Change> {
     return new Info(version, timestamp, changeset, uid);
   }
 
-  private void readTag(Map<String, String> tags) throws XMLStreamException {
+  private void readTag(Map<String, Object> tags) throws XMLStreamException {
     String name = reader.getAttributeValue(null, ATTRIBUTE_NAME_KEY);
     String value = reader.getAttributeValue(null, ATTRIBUTE_NAME_VALUE);
     tags.put(name, value);
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/xml/XmlEntitySpliterator.java b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/xml/XmlEntitySpliterator.java
index 754c3907..3bde5e9a 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/xml/XmlEntitySpliterator.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/openstreetmap/xml/XmlEntitySpliterator.java
@@ -183,7 +183,7 @@ public class XmlEntitySpliterator implements Spliterator<Entity> {
     double longitude = Double.parseDouble(reader.getAttributeValue(null, ATTRIBUTE_NAME_LONGITUDE));
 
     // read the content of the node
-    Map<String, String> tags = new HashMap<>();
+    Map<String, Object> tags = new HashMap<>();
     reader.nextTag();
     while (reader.getEventType() == XMLStreamConstants.START_ELEMENT) {
       switch (reader.getLocalName()) {
@@ -204,7 +204,7 @@ public class XmlEntitySpliterator implements Spliterator<Entity> {
     Info info = readInfo();
 
     // read the content of the node
-    Map<String, String> tags = new HashMap<>();
+    Map<String, Object> tags = new HashMap<>();
     List<Long> members = new ArrayList<>();
     reader.nextTag();
     while (reader.getEventType() == XMLStreamConstants.START_ELEMENT) {
@@ -236,7 +236,7 @@ public class XmlEntitySpliterator implements Spliterator<Entity> {
     Info info = readInfo();
 
     // read the content of the node
-    Map<String, String> tags = new HashMap<>();
+    Map<String, Object> tags = new HashMap<>();
     List<Member> members = new ArrayList<>();
     reader.nextTag();
     while (reader.getEventType() == XMLStreamConstants.START_ELEMENT) {
@@ -279,7 +279,7 @@ public class XmlEntitySpliterator implements Spliterator<Entity> {
     return new Info(version, timestamp, changeset, uid);
   }
 
-  private void readTag(Map<String, String> tags) throws XMLStreamException {
+  private void readTag(Map<String, Object> tags) throws XMLStreamException {
     String name = reader.getAttributeValue(null, ATTRIBUTE_NAME_KEY);
     String value = reader.getAttributeValue(null, ATTRIBUTE_NAME_VALUE);
     tags.put(name, value);
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/storage/FeatureSetProjectionTransform.java b/baremaps-core/src/main/java/org/apache/baremaps/storage/FeatureSetProjectionTransform.java
index a7743a6a..b161732f 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/storage/FeatureSetProjectionTransform.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/storage/FeatureSetProjectionTransform.java
@@ -14,78 +14,45 @@ package org.apache.baremaps.storage;
 
 
 
-import java.util.Optional;
+import java.io.IOException;
 import java.util.stream.Stream;
+import org.apache.baremaps.feature.Feature;
+import org.apache.baremaps.feature.FeatureType;
+import org.apache.baremaps.feature.ReadableFeatureSet;
 import org.apache.baremaps.openstreetmap.utils.ProjectionTransformer;
-import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.FeatureSet;
-import org.apache.sis.storage.event.StoreEvent;
-import org.apache.sis.storage.event.StoreListener;
 import org.locationtech.jts.geom.Geometry;
-import org.opengis.feature.Feature;
-import org.opengis.feature.FeatureType;
-import org.opengis.geometry.Envelope;
-import org.opengis.metadata.Metadata;
-import org.opengis.util.GenericName;
 
-public class FeatureSetProjectionTransform implements FeatureSet {
+public class FeatureSetProjectionTransform implements ReadableFeatureSet {
 
-  private final FeatureSet featureSet;
+  private final ReadableFeatureSet featureSetReader;
 
   private final ProjectionTransformer projectionTransformer;
 
-  public FeatureSetProjectionTransform(FeatureSet featureSet,
+  public FeatureSetProjectionTransform(ReadableFeatureSet featureSetReader,
       ProjectionTransformer projectionTransformer) {
-    this.featureSet = featureSet;
+    this.featureSetReader = featureSetReader;
     this.projectionTransformer = projectionTransformer;
   }
 
   @Override
-  public FeatureType getType() throws DataStoreException {
-    return featureSet.getType();
+  public FeatureType getType() throws IOException {
+    return featureSetReader.getType();
   }
 
   @Override
-  public Stream<Feature> features(boolean parallel) throws DataStoreException {
-    return featureSet.features(parallel).map(this::transformProjection);
+  public Stream<Feature> read() throws IOException {
+    return featureSetReader.read().map(this::transformProjection);
   }
 
   public Feature transformProjection(Feature feature) {
-    for (var property : feature.getType().getProperties(true)) {
-      var name = property.getName().toString();
-      var value = feature.getPropertyValue(name);
+    for (var property : feature.getType().getProperties().values()) {
+      var name = property.getName();
+      var value = feature.getProperty(name);
       if (value instanceof Geometry geometry) {
         var projectedGeometry = projectionTransformer.transform(geometry);
-        feature.setPropertyValue(name, projectedGeometry);
+        feature.setProperty(name, projectedGeometry);
       }
     }
     return feature;
   }
-
-  @Override
-  public Optional<Envelope> getEnvelope() throws DataStoreException {
-    return featureSet.getEnvelope();
-  }
-
-  @Override
-  public Optional<GenericName> getIdentifier() throws DataStoreException {
-    return featureSet.getIdentifier();
-  }
-
-  @Override
-  public Metadata getMetadata() throws DataStoreException {
-    return featureSet.getMetadata();
-  }
-
-  @Override
-  public <T extends StoreEvent> void addListener(Class<T> eventType,
-      StoreListener<? super T> listener) {
-    featureSet.addListener(eventType, listener);
-  }
-
-  @Override
-  public <T extends StoreEvent> void removeListener(Class<T> eventType,
-      StoreListener<? super T> listener) {
-    featureSet.removeListener(eventType, listener);
-  }
 }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/storage/geopackage/GeoPackageDatabase.java b/baremaps-core/src/main/java/org/apache/baremaps/storage/geopackage/GeoPackageDatabase.java
index ac0c30ad..b405315a 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/storage/geopackage/GeoPackageDatabase.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/storage/geopackage/GeoPackageDatabase.java
@@ -14,21 +14,16 @@ package org.apache.baremaps.storage.geopackage;
 
 
 
+import java.io.IOException;
 import java.nio.file.Path;
-import java.util.Collection;
-import java.util.Optional;
-import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import mil.nga.geopackage.GeoPackage;
 import mil.nga.geopackage.GeoPackageManager;
-import org.apache.sis.storage.Aggregate;
-import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.Resource;
-import org.apache.sis.storage.event.StoreEvent;
-import org.apache.sis.storage.event.StoreListener;
-import org.opengis.metadata.Metadata;
-import org.opengis.util.GenericName;
+import org.apache.baremaps.feature.ReadableAggregate;
+import org.apache.baremaps.feature.Resource;
 
-public class GeoPackageDatabase implements Aggregate, AutoCloseable {
+
+public class GeoPackageDatabase implements ReadableAggregate, AutoCloseable {
 
   private final GeoPackage geoPackage;
 
@@ -37,32 +32,9 @@ public class GeoPackageDatabase implements Aggregate, AutoCloseable {
   }
 
   @Override
-  public Optional<GenericName> getIdentifier() throws DataStoreException {
-    return Optional.empty();
-  }
-
-  @Override
-  public Metadata getMetadata() throws DataStoreException {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public <T extends StoreEvent> void addListener(Class<T> eventType,
-      StoreListener<? super T> listener) {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public <T extends StoreEvent> void removeListener(Class<T> eventType,
-      StoreListener<? super T> listener) {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public Collection<? extends Resource> components() throws DataStoreException {
+  public Stream<Resource> read() throws IOException {
     return geoPackage.getFeatureTables().stream()
-        .map(table -> new GeoPackageTable(geoPackage.getFeatureDao(table)))
-        .collect(Collectors.toList());
+        .map(table -> new GeoPackageTable(geoPackage.getFeatureDao(table)));
   }
 
   @Override
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/storage/geopackage/GeoPackageTable.java b/baremaps-core/src/main/java/org/apache/baremaps/storage/geopackage/GeoPackageTable.java
index c0f7f308..148adde0 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/storage/geopackage/GeoPackageTable.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/storage/geopackage/GeoPackageTable.java
@@ -14,24 +14,18 @@ package org.apache.baremaps.storage.geopackage;
 
 
 
-import java.util.Date;
-import java.util.Iterator;
-import java.util.List;
-import java.util.NoSuchElementException;
-import java.util.Optional;
-import java.util.Spliterators;
+import java.io.IOException;
+import java.util.*;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 import mil.nga.geopackage.features.user.FeatureColumn;
 import mil.nga.geopackage.features.user.FeatureDao;
 import mil.nga.geopackage.features.user.FeatureResultSet;
 import mil.nga.geopackage.geom.GeoPackageGeometryData;
-import org.apache.sis.feature.builder.AttributeRole;
-import org.apache.sis.feature.builder.FeatureTypeBuilder;
-import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.FeatureSet;
-import org.apache.sis.storage.event.StoreEvent;
-import org.apache.sis.storage.event.StoreListener;
+import org.apache.baremaps.feature.Feature;
+import org.apache.baremaps.feature.FeatureType;
+import org.apache.baremaps.feature.PropertyType;
+import org.apache.baremaps.feature.ReadableFeatureSet;
 import org.locationtech.jts.geom.Coordinate;
 import org.locationtech.jts.geom.Geometry;
 import org.locationtech.jts.geom.GeometryCollection;
@@ -44,13 +38,8 @@ import org.locationtech.jts.geom.MultiPolygon;
 import org.locationtech.jts.geom.Point;
 import org.locationtech.jts.geom.Polygon;
 import org.locationtech.jts.geom.PrecisionModel;
-import org.opengis.feature.Feature;
-import org.opengis.feature.FeatureType;
-import org.opengis.geometry.Envelope;
-import org.opengis.metadata.Metadata;
-import org.opengis.util.GenericName;
 
-public class GeoPackageTable implements FeatureSet {
+public class GeoPackageTable implements ReadableFeatureSet {
 
   private final FeatureDao featureDao;
 
@@ -60,15 +49,14 @@ public class GeoPackageTable implements FeatureSet {
 
   protected GeoPackageTable(FeatureDao featureDao) {
     this.featureDao = featureDao;
-    var typeBuilder = new FeatureTypeBuilder().setName(featureDao.getTableName());
+    var name = featureDao.getTableName();
+    var properties = new HashMap<String, PropertyType>();
     for (FeatureColumn column : featureDao.getColumns()) {
-      var attributeBuilder = typeBuilder.addAttribute(classType(column)).setName(column.getName())
-          .setMinimumOccurs(column.isNotNull() ? 1 : 0);
-      if (column.isPrimaryKey()) {
-        attributeBuilder.addRole(AttributeRole.IDENTIFIER_COMPONENT);
-      }
+      var propertyName = column.getName();
+      var propertyType = classType(column);
+      properties.put(propertyName, new PropertyType(name, propertyType));
     }
-    featureType = typeBuilder.build();
+    featureType = new FeatureType(name, properties);
     geometryFactory = new GeometryFactory(new PrecisionModel(), (int) featureDao.getSrs().getId());
   }
 
@@ -81,40 +69,13 @@ public class GeoPackageTable implements FeatureSet {
   }
 
   @Override
-  public Optional<Envelope> getEnvelope() throws DataStoreException {
-    return Optional.empty();
-  }
-
-  @Override
-  public Optional<GenericName> getIdentifier() throws DataStoreException {
-    return Optional.empty();
-  }
-
-  @Override
-  public Metadata getMetadata() throws DataStoreException {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public <T extends StoreEvent> void addListener(Class<T> eventType,
-      StoreListener<? super T> listener) {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public <T extends StoreEvent> void removeListener(Class<T> eventType,
-      StoreListener<? super T> listener) {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public FeatureType getType() throws DataStoreException {
+  public FeatureType getType() throws IOException {
     return featureType;
   }
 
   @Override
-  public Stream<Feature> features(boolean parallel) throws DataStoreException {
-    Iterator<Feature> featureIterator = new FeatureIterator(featureDao.queryForAll(), featureType);
+  public Stream<Feature> read() throws IOException {
+    var featureIterator = new FeatureIterator(featureDao.queryForAll(), featureType);
     return StreamSupport.stream(Spliterators.spliteratorUnknownSize(featureIterator, 0), false);
   }
 
@@ -146,7 +107,7 @@ public class GeoPackageTable implements FeatureSet {
       for (FeatureColumn featureColumn : featureResultSet.getColumns().getColumns()) {
         var value = featureResultSet.getValue(featureColumn);
         if (value != null) {
-          feature.setPropertyValue(featureColumn.getName(), asValue(value));
+          feature.setProperty(featureColumn.getName(), asValue(value));
         }
       }
       hasNext = featureResultSet.moveToNext();
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/storage/postgres/PostgresDatabase.java b/baremaps-core/src/main/java/org/apache/baremaps/storage/postgres/PostgresDatabase.java
index 22a58ef6..4c6ae4ce 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/storage/postgres/PostgresDatabase.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/storage/postgres/PostgresDatabase.java
@@ -15,40 +15,21 @@ package org.apache.baremaps.storage.postgres;
 
 
 import de.bytefish.pgbulkinsert.pgsql.handlers.*;
+import java.io.IOException;
 import java.sql.SQLException;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
-import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 import javax.sql.DataSource;
 import org.apache.baremaps.database.copy.CopyWriter;
 import org.apache.baremaps.database.copy.PostgisGeometryValueHandler;
-import org.apache.sis.feature.builder.FeatureTypeBuilder;
-import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.FeatureSet;
-import org.apache.sis.storage.Resource;
-import org.apache.sis.storage.WritableAggregate;
-import org.apache.sis.storage.event.StoreEvent;
-import org.apache.sis.storage.event.StoreListener;
-import org.locationtech.jts.geom.Geometry;
-import org.locationtech.jts.geom.GeometryCollection;
-import org.locationtech.jts.geom.LineString;
-import org.locationtech.jts.geom.LinearRing;
-import org.locationtech.jts.geom.MultiLineString;
-import org.locationtech.jts.geom.MultiPoint;
-import org.locationtech.jts.geom.MultiPolygon;
-import org.locationtech.jts.geom.Point;
-import org.locationtech.jts.geom.Polygon;
-import org.opengis.feature.AttributeType;
-import org.opengis.feature.FeatureType;
-import org.opengis.feature.PropertyType;
-import org.opengis.metadata.Metadata;
-import org.opengis.util.GenericName;
+import org.apache.baremaps.feature.*;
+import org.locationtech.jts.geom.*;
 import org.postgresql.PGConnection;
 import org.postgresql.copy.PGCopyOutputStream;
 
@@ -108,51 +89,19 @@ public class PostgresDatabase implements WritableAggregate {
     this.dataSource = dataSource;
   }
 
-  @Override
-  public Collection<? extends Resource> components() throws DataStoreException {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public Optional<GenericName> getIdentifier() throws DataStoreException {
-    return Optional.empty();
-  }
-
-  @Override
-  public Metadata getMetadata() throws DataStoreException {
-    throw new UnsupportedOperationException();
+  private FeatureType createFeatureType(FeatureType featureType) {
+    var name = featureType.getName().replaceAll("[^a-zA-Z0-9]", "_");
+    var properties = featureType.getProperties().values().stream()
+        .filter(typeToName::containsKey)
+        .collect(Collectors.toMap(PropertyType::getName, Function.identity()));
+    return new FeatureType(name, properties);
   }
 
   @Override
-  public <T extends StoreEvent> void addListener(Class<T> eventType,
-      StoreListener<? super T> listener) {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public <T extends StoreEvent> void removeListener(Class<T> eventType,
-      StoreListener<? super T> listener) {
-    throw new UnsupportedOperationException();
-  }
-
-  public FeatureType createFeatureType(FeatureType featureType) {
-    var featureTypeBuilder = new FeatureTypeBuilder();
-    featureTypeBuilder.setName(featureType.getName().toString().replaceAll("[^a-zA-Z0-9]", "_"));
-    for (var attribute : featureType.getProperties(false)) {
-      if (attribute instanceof AttributeType attributeType
-          && typeToName.containsKey(attributeType.getValueClass())) {
-        featureTypeBuilder.addAttribute(attributeType.getValueClass()).setName(attribute.getName());
-      }
-    }
-    return featureTypeBuilder.build();
-  }
-
-  @Override
-  public Resource add(Resource resource) throws DataStoreException {
-    if (resource instanceof FeatureSet featureSet) {
-
+  public void write(Resource resource) throws IOException {
+    if (resource instanceof ReadableFeatureSet featureSetReader) {
       try (var connection = dataSource.getConnection()) {
-        var featureType = createFeatureType(featureSet.getType());
+        var featureType = createFeatureType(featureSetReader.getType());
 
         // Drop the table if it exists
         var dropQuery = dropTable(featureType);
@@ -171,14 +120,14 @@ public class PostgresDatabase implements WritableAggregate {
         var copyQuery = copyTable(featureType);
         try (var writer = new CopyWriter(new PGCopyOutputStream(pgConnection, copyQuery))) {
           writer.writeHeader();
-          var featureIterator = featureSet.features(false).iterator();
+          var featureIterator = featureSetReader.read().iterator();
           while (featureIterator.hasNext()) {
             var feature = featureIterator.next();
             var attributes = getAttributes(featureType);
             writer.startRow(attributes.size());
             for (var attribute : attributes) {
               var name = attribute.getName().toString();
-              var value = feature.getPropertyValue(name);
+              var value = feature.getProperty(name);
               if (value == null) {
                 writer.writeNull();
               } else {
@@ -187,31 +136,19 @@ public class PostgresDatabase implements WritableAggregate {
             }
           }
         }
-
-        return null;
-      } catch (Exception e) {
-        throw new DataStoreException(e);
+      } catch (SQLException e) {
+        throw new IOException(e);
       }
-    } else {
-      throw new DataStoreException("Unsupported resource type");
     }
   }
 
-  private List<AttributeType> getAttributes(FeatureType featureType) {
-    return featureType.getProperties(false).stream().filter(this::isAttribute)
-        .map(this::asAttribute).filter(this::isSupported).collect(Collectors.toList());
-  }
-
-  private boolean isAttribute(PropertyType propertyType) {
-    return propertyType instanceof AttributeType;
-  }
-
-  private AttributeType asAttribute(PropertyType propertyType) {
-    return (AttributeType) propertyType;
+  private List<PropertyType> getAttributes(FeatureType featureType) {
+    return featureType.getProperties().values().stream()
+        .filter(this::isSupported).collect(Collectors.toList());
   }
 
-  private boolean isSupported(AttributeType attributeType) {
-    return typeToName.containsKey(attributeType.getValueClass());
+  private boolean isSupported(PropertyType propertyType) {
+    return typeToName.containsKey(propertyType.getType());
   }
 
   private String createTable(FeatureType featureType) {
@@ -219,9 +156,9 @@ public class PostgresDatabase implements WritableAggregate {
     builder.append("CREATE TABLE ");
     builder.append(featureType.getName());
     builder.append(" (");
-    builder.append(featureType.getProperties(false).stream().filter(AttributeType.class::isInstance)
-        .map(AttributeType.class::cast).map(attributeType -> attributeType.getName().toString()
-            + " " + typeToName.get(attributeType.getValueClass()))
+    builder.append(featureType.getProperties().values().stream()
+        .map(attributeType -> attributeType.getName()
+            + " " + typeToName.get(attributeType.getType()))
         .collect(Collectors.joining(", ")));
     builder.append(")");
     return builder.toString();
@@ -232,15 +169,15 @@ public class PostgresDatabase implements WritableAggregate {
     builder.append("COPY ");
     builder.append(featureType.getName());
     builder.append(" (");
-    builder.append(featureType.getProperties(false).stream().filter(AttributeType.class::isInstance)
-        .map(AttributeType.class::cast).map(attributeType -> attributeType.getName().toString())
+    builder.append(featureType.getProperties().values().stream()
+        .map(propertyType -> propertyType.getName())
         .collect(Collectors.joining(", ")));
     builder.append(") FROM STDIN BINARY");
     return builder.toString();
   }
 
   @Override
-  public void remove(Resource resource) throws DataStoreException {
+  public void remove(Resource resource) throws IOException {
     if (resource instanceof FeatureSet featureSet) {
       var type = featureSet.getType();
       try (var connection = dataSource.getConnection();
@@ -249,8 +186,6 @@ public class PostgresDatabase implements WritableAggregate {
       } catch (SQLException e) {
         throw new RuntimeException(e);
       }
-    } else {
-      throw new DataStoreException("Unsupported resource type");
     }
   }
 
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/storage/postgres/PostgresTable.java b/baremaps-core/src/main/java/org/apache/baremaps/storage/postgres/PostgresTable.java
index 22339a8e..d7f8407a 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/storage/postgres/PostgresTable.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/storage/postgres/PostgresTable.java
@@ -14,20 +14,11 @@ package org.apache.baremaps.storage.postgres;
 
 
 
-import java.util.Iterator;
-import java.util.Optional;
-import java.util.function.Predicate;
-import java.util.function.UnaryOperator;
+import java.io.IOException;
 import java.util.stream.Stream;
-import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.WritableFeatureSet;
-import org.apache.sis.storage.event.StoreEvent;
-import org.apache.sis.storage.event.StoreListener;
-import org.opengis.feature.Feature;
-import org.opengis.feature.FeatureType;
-import org.opengis.geometry.Envelope;
-import org.opengis.metadata.Metadata;
-import org.opengis.util.GenericName;
+import org.apache.baremaps.feature.Feature;
+import org.apache.baremaps.feature.FeatureType;
+import org.apache.baremaps.feature.WritableFeatureSet;
 
 public class PostgresTable implements WritableFeatureSet {
 
@@ -38,56 +29,12 @@ public class PostgresTable implements WritableFeatureSet {
   }
 
   @Override
-  public void updateType(FeatureType newType) throws DataStoreException {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public void add(Iterator<? extends Feature> features) throws DataStoreException {}
-
-  @Override
-  public boolean removeIf(Predicate<? super Feature> filter) throws DataStoreException {
-    return false;
-  }
-
-  @Override
-  public void replaceIf(Predicate<? super Feature> filter, UnaryOperator<Feature> updater)
-      throws DataStoreException {}
-
-  @Override
-  public FeatureType getType() throws DataStoreException {
-    return null;
-  }
-
-  @Override
-  public Stream<Feature> features(boolean parallel) throws DataStoreException {
-    return null;
-  }
-
-  @Override
-  public Optional<Envelope> getEnvelope() throws DataStoreException {
-    return Optional.empty();
-  }
-
-  @Override
-  public Optional<GenericName> getIdentifier() throws DataStoreException {
-    return Optional.empty();
-  }
-
-  @Override
-  public Metadata getMetadata() throws DataStoreException {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public <T extends StoreEvent> void addListener(Class<T> eventType,
-      StoreListener<? super T> listener) {
-    throw new UnsupportedOperationException();
+  public FeatureType getType() throws IOException {
+    return featureType;
   }
 
   @Override
-  public <T extends StoreEvent> void removeListener(Class<T> eventType,
-      StoreListener<? super T> listener) {
+  public void write(Stream<Feature> features) throws IOException {
     throw new UnsupportedOperationException();
   }
 }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/ShapefileDirectory.java b/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/ShapefileDirectory.java
index ed52b372..a187b7e1 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/ShapefileDirectory.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/ShapefileDirectory.java
@@ -17,18 +17,11 @@ package org.apache.baremaps.storage.shapefile;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.Collection;
-import java.util.Optional;
-import java.util.stream.Collectors;
-import org.apache.sis.storage.Aggregate;
-import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.Resource;
-import org.apache.sis.storage.event.StoreEvent;
-import org.apache.sis.storage.event.StoreListener;
-import org.opengis.metadata.Metadata;
-import org.opengis.util.GenericName;
+import java.util.stream.Stream;
+import org.apache.baremaps.feature.ReadableAggregate;
+import org.apache.baremaps.feature.Resource;
 
-public class ShapefileDirectory implements Aggregate {
+public class ShapefileDirectory implements ReadableAggregate {
 
   private final Path directory;
 
@@ -37,34 +30,10 @@ public class ShapefileDirectory implements Aggregate {
   }
 
   @Override
-  public Collection<? extends Resource> components() throws DataStoreException {
-    try (var list = Files.list(directory)) {
-      return list.filter(file -> file.toString().toLowerCase().endsWith(".shp"))
-          .map(file -> new ShapefileFeatureSet(file)).collect(Collectors.toList());
-    } catch (IOException e) {
-      throw new DataStoreException(e);
-    }
-  }
-
-  @Override
-  public Optional<GenericName> getIdentifier() throws DataStoreException {
-    return Optional.empty();
-  }
+  public Stream<Resource> read() throws IOException {
+    return Files.list(directory)
+        .filter(file -> file.toString().toLowerCase().endsWith(".shp"))
+        .map(file -> new ShapefileFeatureSet(file));
 
-  @Override
-  public Metadata getMetadata() throws DataStoreException {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public <T extends StoreEvent> void addListener(Class<T> eventType,
-      StoreListener<? super T> listener) {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public <T extends StoreEvent> void removeListener(Class<T> eventType,
-      StoreListener<? super T> listener) {
-    throw new UnsupportedOperationException();
   }
 }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/ShapefileFeatureSet.java b/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/ShapefileFeatureSet.java
index ef9b7806..bf688b8b 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/ShapefileFeatureSet.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/ShapefileFeatureSet.java
@@ -14,25 +14,19 @@ package org.apache.baremaps.storage.shapefile;
 
 
 
+import java.io.IOException;
 import java.nio.file.Path;
-import java.util.Optional;
 import java.util.Spliterator;
 import java.util.function.Consumer;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
+import org.apache.baremaps.feature.Feature;
+import org.apache.baremaps.feature.FeatureType;
+import org.apache.baremaps.feature.ReadableFeatureSet;
 import org.apache.baremaps.storage.shapefile.internal.InputFeatureStream;
 import org.apache.baremaps.storage.shapefile.internal.ShapefileReader;
-import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.FeatureSet;
-import org.apache.sis.storage.event.StoreEvent;
-import org.apache.sis.storage.event.StoreListener;
-import org.opengis.feature.Feature;
-import org.opengis.feature.FeatureType;
-import org.opengis.geometry.Envelope;
-import org.opengis.metadata.Metadata;
-import org.opengis.util.GenericName;
-
-public class ShapefileFeatureSet implements FeatureSet, AutoCloseable {
+
+public class ShapefileFeatureSet implements ReadableFeatureSet, AutoCloseable {
 
   private final ShapefileReader shapeFile;
 
@@ -41,55 +35,17 @@ public class ShapefileFeatureSet implements FeatureSet, AutoCloseable {
   }
 
   @Override
-  public FeatureType getType() throws DataStoreException {
+  public FeatureType getType() throws IOException {
     try (var input = shapeFile.read()) {
       return input.getFeaturesType();
-    } catch (Exception e) {
-      throw new DataStoreException(e);
-    }
-  }
-
-  @Override
-  public Stream<Feature> features(boolean parallel) throws DataStoreException {
-    try {
-      var input = shapeFile.read();
-      return StreamSupport.stream(new FeatureSpliterator(shapeFile.read()), false)
-          .onClose(() -> input.close());
-    } catch (Exception e) {
-      throw new DataStoreException(e);
     }
   }
 
   @Override
-  public Optional<Envelope> getEnvelope() throws DataStoreException {
-    return Optional.empty();
-  }
-
-  @Override
-  public Optional<GenericName> getIdentifier() throws DataStoreException {
-    return Optional.empty();
-  }
-
-  @Override
-  public Metadata getMetadata() throws DataStoreException {
-    throw new UnsupportedOperationException();
+  public Stream<Feature> read() throws IOException {
+    return StreamSupport.stream(new FeatureSpliterator(shapeFile.read()), false);
   }
 
-  @Override
-  public <T extends StoreEvent> void addListener(Class<T> eventType,
-      StoreListener<? super T> listener) {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public <T extends StoreEvent> void removeListener(Class<T> eventType,
-      StoreListener<? super T> listener) {
-    throw new UnsupportedOperationException();
-  }
-
-  @Override
-  public void close() throws Exception {}
-
   static class FeatureSpliterator implements Spliterator<Feature> {
 
     private final InputFeatureStream inputFeatureStream;
@@ -128,4 +84,9 @@ public class ShapefileFeatureSet implements FeatureSet, AutoCloseable {
       return 0;
     }
   }
+
+  @Override
+  public void close() throws Exception {
+
+  }
 }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/internal/DbaseByteReader.java b/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/internal/DbaseByteReader.java
index f95df372..13eea12e 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/internal/DbaseByteReader.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/internal/DbaseByteReader.java
@@ -22,7 +22,7 @@ import java.nio.charset.Charset;
 import java.nio.charset.UnsupportedCharsetException;
 import java.text.MessageFormat;
 import java.util.*;
-import org.apache.sis.feature.AbstractFeature;
+import org.apache.baremaps.feature.Feature;
 
 /**
  * Reader of a Database Binary content.
@@ -101,7 +101,7 @@ public class DbaseByteReader extends CommonByteReader implements AutoCloseable {
    *
    * @param feature Feature to fill.
    */
-  public void loadRowIntoFeature(AbstractFeature feature) {
+  public void loadRowIntoFeature(Feature feature) {
     // TODO: ignore deleted records
     getByteBuffer().get(); // denotes whether deleted or current
     // read first part of record
@@ -138,7 +138,7 @@ public class DbaseByteReader extends CommonByteReader implements AutoCloseable {
         case DateTime -> value;
       };
 
-      feature.setPropertyValue(fd.getName(), object);
+      feature.setProperty(fd.getName(), object);
     }
   }
 
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/internal/InputFeatureStream.java b/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/internal/InputFeatureStream.java
index f24fb23a..89849c6a 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/internal/InputFeatureStream.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/internal/InputFeatureStream.java
@@ -19,8 +19,8 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.sql.SQLFeatureNotSupportedException;
 import java.util.List;
-import org.apache.sis.feature.AbstractFeature;
-import org.apache.sis.feature.DefaultFeatureType;
+import org.apache.baremaps.feature.Feature;
+import org.apache.baremaps.feature.FeatureType;
 
 /**
  * Input Stream of features.
@@ -48,7 +48,7 @@ public class InputFeatureStream extends InputStream {
   private boolean hasShapefileIndex;
 
   /** Type of the features contained in this shapefile. */
-  private DefaultFeatureType featuresType;
+  private FeatureType featuresType;
 
   /** Shapefile reader. */
   private ShapefileByteReader shapefileReader;
@@ -113,7 +113,7 @@ public class InputFeatureStream extends InputStream {
    * @throws ShapefileException if the current connection used to query the shapefile has been
    *         closed.
    */
-  public AbstractFeature readFeature() throws ShapefileException {
+  public Feature readFeature() throws ShapefileException {
     return internalReadFeature();
   }
 
@@ -122,8 +122,8 @@ public class InputFeatureStream extends InputStream {
    *
    * @return Features type.
    */
-  public DefaultFeatureType getFeaturesType() {
-    return this.featuresType;
+  public FeatureType getFeaturesType() {
+    return featuresType;
   }
 
   /**
@@ -160,11 +160,11 @@ public class InputFeatureStream extends InputStream {
    * @throws SQLFeatureNotSupportedException if a SQL ability is not currently available through
    *         this driver.
    */
-  private AbstractFeature internalReadFeature() throws ShapefileException {
+  private Feature internalReadFeature() throws ShapefileException {
     if (!this.dbaseReader.nextRowAvailable()) {
       return null;
     }
-    AbstractFeature feature = (AbstractFeature) this.featuresType.newInstance();
+    Feature feature = this.featuresType.newInstance();
     this.dbaseReader.loadRowIntoFeature(feature);
     this.shapefileReader.setRowNum(this.dbaseReader.getRowNum());
     this.shapefileReader.completeFeature(feature);
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/internal/ShapefileByteReader.java b/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/internal/ShapefileByteReader.java
index 4bed9240..d56824af 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/internal/ShapefileByteReader.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/internal/ShapefileByteReader.java
@@ -19,9 +19,9 @@ import java.nio.ByteOrder;
 import java.nio.MappedByteBuffer;
 import java.nio.channels.FileChannel;
 import java.util.*;
-import org.apache.sis.feature.AbstractFeature;
-import org.apache.sis.feature.DefaultAttributeType;
-import org.apache.sis.feature.DefaultFeatureType;
+import org.apache.baremaps.feature.Feature;
+import org.apache.baremaps.feature.FeatureType;
+import org.apache.baremaps.feature.PropertyType;
 import org.locationtech.jts.algorithm.Orientation;
 import org.locationtech.jts.geom.Coordinate;
 import org.locationtech.jts.geom.CoordinateList;
@@ -47,7 +47,7 @@ public class ShapefileByteReader extends CommonByteReader {
   private List<DBaseFieldDescriptor> databaseFieldsDescriptors;
 
   /** Type of the features contained in this shapefile. */
-  private DefaultFeatureType featuresType;
+  private FeatureType featuresType;
 
   /** Shapefile index. */
   private File shapeFileIndex;
@@ -108,7 +108,7 @@ public class ShapefileByteReader extends CommonByteReader {
    *
    * @return Features type.
    */
-  public DefaultFeatureType getFeaturesType() {
+  public FeatureType getFeaturesType() {
     return this.featuresType;
   }
 
@@ -118,20 +118,15 @@ public class ShapefileByteReader extends CommonByteReader {
    * @param name Name of the field.
    * @return The feature type.
    */
-  private DefaultFeatureType getFeatureType(final String name) {
+  private FeatureType getFeatureType(final String name) {
     Objects.requireNonNull(name, "The feature name cannot be null.");
 
-    final int n = this.databaseFieldsDescriptors.size();
-    final DefaultAttributeType<?>[] attributes = new DefaultAttributeType<?>[n + 1];
-    final Map<String, Object> properties = new HashMap<>(4);
-
-    // Load data field.
-    for (int i = 0; i < n; i++) {
+    var properties = new HashMap<String, PropertyType>();
+    for (int i = 0; i < databaseFieldsDescriptors.size(); i++) {
       var fieldDescriptor = this.databaseFieldsDescriptors.get(i);
-      properties.put(DefaultAttributeType.NAME_KEY, fieldDescriptor.getName());
 
-      // TODO: move somewhere else
-      Class type = switch (fieldDescriptor.getType()) {
+      var propertyName = fieldDescriptor.getName();
+      var propertyType = switch (fieldDescriptor.getType()) {
         case Character -> String.class;
         case Number -> fieldDescriptor.getDecimalCount() == 0 ? Long.class : Double.class;
         case Currency -> Double.class;
@@ -151,16 +146,13 @@ public class ShapefileByteReader extends CommonByteReader {
         case DateTime -> String.class;
       };
 
-      attributes[i] = new DefaultAttributeType<>(properties, type, 1, 1, null);
+      properties.put(propertyName, new PropertyType(propertyName, propertyType));
     }
 
     // Add geometry field.
-    properties.put(DefaultAttributeType.NAME_KEY, GEOMETRY_NAME);
-    attributes[n] = new DefaultAttributeType<>(properties, Geometry.class, 1, 1, null);
+    properties.put(GEOMETRY_NAME, new PropertyType(GEOMETRY_NAME, Geometry.class));
 
-    // Add name.
-    properties.put(DefaultAttributeType.NAME_KEY, name);
-    return new DefaultFeatureType(properties, false, null, attributes);
+    return new FeatureType(name, properties);
   }
 
   /** Load shapefile descriptor. */
@@ -267,7 +259,7 @@ public class ShapefileByteReader extends CommonByteReader {
    *
    * @param feature Feature to complete.
    */
-  public void completeFeature(AbstractFeature feature) throws ShapefileException {
+  public void completeFeature(Feature feature) throws ShapefileException {
     // insert points into some type of list
     int RecordNumber = getByteBuffer().getInt();
     int ContentLength = getByteBuffer().getInt();
@@ -307,11 +299,11 @@ public class ShapefileByteReader extends CommonByteReader {
    *
    * @param feature Feature to fill.
    */
-  private void loadPointFeature(AbstractFeature feature) {
+  private void loadPointFeature(Feature feature) {
     double x = getByteBuffer().getDouble();
     double y = getByteBuffer().getDouble();
     Point pnt = geometryFactory.createPoint(new Coordinate(x, y));
-    feature.setPropertyValue(GEOMETRY_NAME, pnt);
+    feature.setProperty(GEOMETRY_NAME, pnt);
   }
 
   /**
@@ -319,7 +311,7 @@ public class ShapefileByteReader extends CommonByteReader {
    *
    * @param feature Feature to fill.
    */
-  private void loadPolygonFeature(AbstractFeature feature) {
+  private void loadPolygonFeature(Feature feature) {
     double xmin = getByteBuffer().getDouble();
     double ymin = getByteBuffer().getDouble();
     double xmax = getByteBuffer().getDouble();
@@ -330,7 +322,7 @@ public class ShapefileByteReader extends CommonByteReader {
 
     Geometry multiPolygon = readMultiplePolygon(numParts, numPoints);
 
-    feature.setPropertyValue(GEOMETRY_NAME, multiPolygon);
+    feature.setProperty(GEOMETRY_NAME, multiPolygon);
   }
 
   /**
@@ -395,7 +387,7 @@ public class ShapefileByteReader extends CommonByteReader {
    *
    * @param feature Feature to fill.
    */
-  private void loadPolylineFeature(AbstractFeature feature) {
+  private void loadPolylineFeature(Feature feature) {
     /* double xmin = */ getByteBuffer().getDouble();
     /* double ymin = */ getByteBuffer().getDouble();
     /* double xmax = */ getByteBuffer().getDouble();
@@ -427,7 +419,7 @@ public class ShapefileByteReader extends CommonByteReader {
       }
     }
 
-    feature.setPropertyValue(GEOMETRY_NAME,
+    feature.setProperty(GEOMETRY_NAME,
         geometryFactory.createLineString(coordinates.toCoordinateArray()));
   }
 }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/internal/ShapefileReader.java b/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/internal/ShapefileReader.java
index 511eee51..97625402 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/internal/ShapefileReader.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/storage/shapefile/internal/ShapefileReader.java
@@ -18,7 +18,7 @@ import java.io.File;
 import java.io.IOException;
 import java.util.List;
 import java.util.Objects;
-import org.apache.sis.feature.DefaultFeatureType;
+import org.apache.baremaps.feature.FeatureType;
 
 /**
  * Provides a ShapeFile Reader.
@@ -44,7 +44,7 @@ public class ShapefileReader {
   private File shapeFileIndex;
 
   /** Type of the features contained in this shapefile. */
-  private DefaultFeatureType featuresType;
+  private FeatureType featuresType;
 
   /** Shapefile descriptor. */
   private ShapefileDescriptor shapefileDescriptor;
@@ -117,7 +117,7 @@ public class ShapefileReader {
    *
    * @return Feature type.
    */
-  public DefaultFeatureType getFeaturesType() {
+  public FeatureType getFeaturesType() {
     return this.featuresType;
   }
 
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java b/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java
index 5f4081ba..149db840 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/stream/ConsumerUtils.java
@@ -22,6 +22,18 @@ public class ConsumerUtils {
 
   private ConsumerUtils() {}
 
+  /**
+   * Returns a consumer that applies a function to its input, and then passes the result to the
+   * 
+   * @param type the type of the input
+   * @return
+   * @param <T>
+   */
+  public static <T> Consumer<T> chain(Class<T> type) {
+    return T -> {
+    };
+  }
+
   /**
    * Transforms a consumer into a function.
    *
@@ -35,4 +47,7 @@ public class ConsumerUtils {
       return t;
     };
   }
+
+
+
 }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/CreateGeonamesIndex.java b/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/CreateGeonamesIndex.java
index 95dd0b78..79d1c400 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/CreateGeonamesIndex.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/CreateGeonamesIndex.java
@@ -19,7 +19,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import java.nio.file.Path;
 
-public record CreateGeonamesIndex(String geonamesDumpPath, String targetGeonamesIndexPath) implements Task {
+public record CreateGeonamesIndex(Path geonamesDumpPath, Path targetGeonamesIndexPath) implements Task {
 
   private static final Logger logger = LoggerFactory.getLogger(CreateGeonamesIndex.class);
 
@@ -27,7 +27,7 @@ public record CreateGeonamesIndex(String geonamesDumpPath, String targetGeonames
   public void execute(WorkflowContext context) throws Exception {
     logger.info("Generating geonames from {}", geonamesDumpPath);
     try (GeonamesGeocoder geocoder =
-                 new GeonamesGeocoder(Path.of(targetGeonamesIndexPath), Path.of(geonamesDumpPath))) {
+                 new GeonamesGeocoder(targetGeonamesIndexPath, geonamesDumpPath)) {
       if (!geocoder.indexExists()) {
         logger.info("Building the geocoder index");
         geocoder.build();
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/CreateIplocIndex.java b/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/CreateIplocIndex.java
index 7b655917..cbcff4fd 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/CreateIplocIndex.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/CreateIplocIndex.java
@@ -30,8 +30,8 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.List;
 
-public record CreateIplocIndex(String geonamesIndexPath, List<String> nicPaths,
-                               String targetIplocIndexPath) implements Task {
+public record CreateIplocIndex(Path geonamesIndexPath, List<Path> nicPaths,
+                               Path targetIplocIndexPath) implements Task {
 
     private static final Logger logger = LoggerFactory.getLogger(CreateIplocIndex.class);
 
@@ -42,7 +42,7 @@ public record CreateIplocIndex(String geonamesIndexPath, List<String> nicPaths,
         logger.info("Creating the Geocoder");
         GeonamesGeocoder geocoder;
         try {
-            geocoder = new GeonamesGeocoder(Path.of(geonamesIndexPath), null);
+            geocoder = new GeonamesGeocoder(geonamesIndexPath, null);
             if (!geocoder.indexExists()) {
                 logger.error("Geocoder index doesn't exist");
                 return;
@@ -61,7 +61,7 @@ public record CreateIplocIndex(String geonamesIndexPath, List<String> nicPaths,
 
         logger.info("Generating NIC objects stream");
         nicPaths.stream().parallel().forEach(path -> {
-          try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(Path.of(path)));) {
+          try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(path));) {
             var nicObjects = NicParser.parse(inputStream);
             logger.info("Inserting the nic objects into the Iploc database");
             ipLoc.insertNicObjects(nicObjects);
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/DownloadUrl.java b/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/DownloadUrl.java
index 5b3001ec..8253d0b5 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/DownloadUrl.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/DownloadUrl.java
@@ -18,14 +18,15 @@ import org.apache.baremaps.workflow.WorkflowContext;
 import java.net.HttpURLConnection;
 import java.net.URL;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.file.StandardCopyOption;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public record DownloadUrl(String url, String path, boolean replaceExisting) implements Task {
+public record DownloadUrl(String url, Path path, boolean replaceExisting) implements Task {
 
-  public DownloadUrl(String url, String path) {
+  public DownloadUrl(String url, Path path) {
     this(url, path, false);
   }
 
@@ -36,7 +37,7 @@ public record DownloadUrl(String url, String path, boolean replaceExisting) impl
     logger.info("Downloading {} to {}", url, path);
 
     var targetUrl = new URL(url);
-    var targetPath = Paths.get(path);
+    var targetPath = path.toAbsolutePath();
 
     if (Files.exists(targetPath) && !replaceExisting) {
       var head = (HttpURLConnection) targetUrl.openConnection();
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/ExecuteSql.java b/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/ExecuteSql.java
index fb3f8a1b..1018ff91 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/ExecuteSql.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/ExecuteSql.java
@@ -17,6 +17,7 @@ import org.apache.baremaps.workflow.WorkflowContext;
 import org.apache.baremaps.workflow.WorkflowException;
 
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.sql.SQLException;
 import java.util.Arrays;
@@ -24,14 +25,14 @@ import java.util.Arrays;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public record ExecuteSql(String database, String file, boolean parallel) implements Task {
+public record ExecuteSql(String database, Path file, boolean parallel) implements Task {
 
   private static final Logger logger = LoggerFactory.getLogger(ExecuteSql.class);
 
   @Override
   public void execute(WorkflowContext context) throws Exception {
     logger.info("Executing {}", file);
-    var queries = Arrays.stream(Files.readString(Paths.get(file)).split(";"));
+    var queries = Arrays.stream(Files.readString(file).split(";"));
     if (parallel) {
       queries = queries.parallel();
     }
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/ExportVectorTiles.java b/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/ExportVectorTiles.java
index e806281a..e7cce007 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/ExportVectorTiles.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/ExportVectorTiles.java
@@ -19,7 +19,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import java.io.IOException;
 import java.nio.file.Files;
-import java.nio.file.Paths;
+import java.nio.file.Path;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -33,8 +33,8 @@ import org.apache.baremaps.database.tile.TileStore;
 import org.apache.baremaps.database.tile.TileStoreException;
 import org.apache.baremaps.openstreetmap.utils.StreamProgress;
 import org.apache.baremaps.stream.StreamUtils;
-import org.apache.baremaps.tileset.Tileset;
-import org.apache.baremaps.tileset.TilesetQuery;
+import org.apache.baremaps.mvt.tileset.Tileset;
+import org.apache.baremaps.mvt.tileset.TilesetQuery;
 import org.apache.baremaps.workflow.Task;
 import org.apache.baremaps.workflow.WorkflowContext;
 import org.locationtech.jts.geom.Envelope;
@@ -44,8 +44,8 @@ import org.sqlite.SQLiteDataSource;
 
 public record ExportVectorTiles(
   String database,
-  String tileset,
-  String repository,
+  Path tileset,
+  Path repository,
   int batchArraySize,
   int batchArrayIndex,
   boolean mbtiles
@@ -64,7 +64,7 @@ public record ExportVectorTiles(
         .setSerializationInclusion(Include.NON_NULL)
         .setSerializationInclusion(Include.NON_EMPTY);
 
-    var source = mapper.readValue(Files.readAllBytes(Paths.get(tileset)), Tileset.class);
+    var source = mapper.readValue(Files.readAllBytes(tileset), Tileset.class);
     var tileSource = sourceTileStore(source, datasource);
     var tileTarget = targetTileStore(source);
 
@@ -100,7 +100,7 @@ public record ExportVectorTiles(
 
       return tilesStore;
     } else {
-      return new FileTileStore(Paths.get(repository));
+      return new FileTileStore(repository);
     }
   }
 
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/ImportGeoPackage.java b/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/ImportGeoPackage.java
index f6f9ec99..7995db97 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/ImportGeoPackage.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/ImportGeoPackage.java
@@ -12,6 +12,7 @@
 
 package org.apache.baremaps.workflow.tasks;
 
+import org.apache.baremaps.feature.ReadableFeatureSet;
 import org.apache.baremaps.openstreetmap.utils.ProjectionTransformer;
 import org.apache.baremaps.storage.FeatureSetProjectionTransform;
 import org.apache.baremaps.storage.geopackage.GeoPackageDatabase;
@@ -20,13 +21,11 @@ import org.apache.baremaps.workflow.Task;
 import org.apache.baremaps.workflow.WorkflowContext;
 import org.apache.baremaps.workflow.WorkflowException;
 
-import java.nio.file.Paths;
-
-import org.apache.sis.storage.FeatureSet;
+import java.nio.file.Path;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public record ImportGeoPackage(String file, String database, Integer sourceSRID, Integer targetSRID)
+public record ImportGeoPackage(Path file, String database, Integer sourceSRID, Integer targetSRID)
   implements Task {
 
   private static final Logger logger = LoggerFactory.getLogger(ImportGeoPackage.class);
@@ -34,14 +33,15 @@ public record ImportGeoPackage(String file, String database, Integer sourceSRID,
   @Override
   public void execute(WorkflowContext context) throws Exception {
     logger.info("Importing {} into {}", file, database);
-    var path = Paths.get(file).toAbsolutePath();
+    var path = file.toAbsolutePath();
     try (var geoPackageStore = new GeoPackageDatabase(path)) {
       var dataSource = context.getDataSource(database);
       var postgresDatabase = new PostgresDatabase(dataSource);
-      for (var resource : geoPackageStore.components()) {
-        if (resource instanceof FeatureSet featureSet) {
-          postgresDatabase.add(new FeatureSetProjectionTransform(
-            featureSet, new ProjectionTransformer(sourceSRID, targetSRID)));
+      for (var resource : geoPackageStore.read().toList()) {
+        if (resource instanceof ReadableFeatureSet featureSet) {
+          var transformer = new ProjectionTransformer(sourceSRID, targetSRID);
+          var transformedFeatureSet = new FeatureSetProjectionTransform(featureSet, transformer);
+          postgresDatabase.write(transformedFeatureSet);
         }
       }
       logger.info("Finished importing {} into {}", file, database);
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/ImportOpenStreetMap.java b/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/ImportOpenStreetMap.java
index 353f319e..064def24 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/ImportOpenStreetMap.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/ImportOpenStreetMap.java
@@ -25,13 +25,14 @@ import org.apache.baremaps.workflow.Task;
 import org.apache.baremaps.workflow.WorkflowContext;
 
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.Paths;
 
 import org.locationtech.jts.geom.Coordinate;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public record ImportOpenStreetMap(String file, String database, Integer databaseSrid)
+public record ImportOpenStreetMap(Path file, String database, Integer databaseSrid)
   implements Task {
 
   private static final Logger logger = LoggerFactory.getLogger(ImportOpenStreetMap.class);
@@ -41,7 +42,7 @@ public record ImportOpenStreetMap(String file, String database, Integer database
     logger.info("Importing {} into {}", file, database);
 
     var dataSource = context.getDataSource(database);
-    var path = Paths.get(file).toAbsolutePath();
+    var path = file.toAbsolutePath();
 
     var headerRepository = new PostgresHeaderRepository(dataSource);
     var nodeRepository = new PostgresNodeRepository(dataSource);
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/ImportShapefile.java b/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/ImportShapefile.java
index 85ebf59a..a6729b88 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/ImportShapefile.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/ImportShapefile.java
@@ -20,12 +20,12 @@ import org.apache.baremaps.workflow.Task;
 import org.apache.baremaps.workflow.WorkflowContext;
 import org.apache.baremaps.workflow.WorkflowException;
 
-import java.nio.file.Paths;
+import java.nio.file.Path;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public record ImportShapefile(String file, String database, Integer sourceSRID, Integer targetSRID)
+public record ImportShapefile(Path file, String database, Integer sourceSRID, Integer targetSRID)
   implements Task {
 
   private static final Logger logger = LoggerFactory.getLogger(ImportShapefile.class);
@@ -33,11 +33,11 @@ public record ImportShapefile(String file, String database, Integer sourceSRID,
   @Override
   public void execute(WorkflowContext context) throws Exception {
     logger.info("Importing {} into {}", file, database);
-    var path = Paths.get(file);
+    var path = file.toAbsolutePath();
     try (var featureSet = new ShapefileFeatureSet(path)) {
       var dataSource = context.getDataSource(database);
       var postgresDatabase = new PostgresDatabase(dataSource);
-      postgresDatabase.add(new FeatureSetProjectionTransform(
+      postgresDatabase.write(new FeatureSetProjectionTransform(
         featureSet, new ProjectionTransformer(sourceSRID, targetSRID)));
       logger.info("Finished importing {} into {}", file, database);
     } catch (Exception e) {
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/SimplifyOpenStreetMap.java b/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/SimplifyOpenStreetMap.java
new file mode 100644
index 00000000..ffc5f809
--- /dev/null
+++ b/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/SimplifyOpenStreetMap.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed 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.workflow.tasks;
+
+
+
+import com.google.common.base.Predicates;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import org.apache.baremaps.collection.*;
+import org.apache.baremaps.collection.memory.OffHeapMemory;
+import org.apache.baremaps.collection.memory.OnDiskDirectoryMemory;
+import org.apache.baremaps.collection.type.*;
+import org.apache.baremaps.collection.utils.CollectionAdapter;
+import org.apache.baremaps.collection.utils.FileUtils;
+import org.apache.baremaps.openstreetmap.model.Element;
+import org.apache.baremaps.openstreetmap.pbf.PbfBlockReader;
+import org.apache.baremaps.openstreetmap.pbf.PbfEntityReader;
+import org.apache.baremaps.workflow.Task;
+import org.apache.baremaps.workflow.WorkflowContext;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.operation.union.CascadedPolygonUnion;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public record SimplifyOpenStreetMap(Path file, String database, Integer databaseSrid) implements Task {
+
+    private static final Logger logger = LoggerFactory.getLogger(ImportOpenStreetMap.class);
+
+    @Override
+    public void execute(WorkflowContext context) throws Exception {
+        logger.info("Importing {} into {}", file, database);
+
+        var path = file.toAbsolutePath();
+
+        var cacheDir = Files.createTempDirectory(Paths.get("."), "cache_");
+
+        var coordinatesKeysDir = Files.createDirectories(cacheDir.resolve("coordinates_keys"));
+        var coordinatesValsDir = Files.createDirectories(cacheDir.resolve("coordinates_vals"));
+        var coordinateMap =
+                new LongDataSortedMap<>(
+                        new AlignedDataList<>(
+                                new PairDataType<>(new LongDataType(), new LongDataType()),
+                                new OnDiskDirectoryMemory(coordinatesKeysDir)
+                        ),
+                        new DataStore<>(
+                                new LonLatDataType(),
+                                new OnDiskDirectoryMemory(coordinatesValsDir)));
+
+        var referencesKeysDir = Files.createDirectories(cacheDir.resolve("references_keys"));
+        var referencesValuesDir = Files.createDirectories(cacheDir.resolve("references_vals"));
+        var referenceMap =
+                new LongDataSortedMap<>(
+                        new AlignedDataList<>(
+                                new PairDataType<>(new LongDataType(), new LongDataType()),
+                                new OnDiskDirectoryMemory(referencesKeysDir)
+                        ),
+                        new DataStore<>(
+                                new LongListDataType(),
+                                new OnDiskDirectoryMemory(referencesValuesDir)));
+
+        var collection = new IndexedDataList<>(
+                new LongList(new OffHeapMemory()),
+                new DataStore<>(new GeometryDataType(), new OffHeapMemory()));
+
+
+        new PbfEntityReader(
+                new PbfBlockReader()
+                        .geometries(true)
+                        .coordinateMap(coordinateMap)
+                        .referenceMap(referenceMap))
+                .stream(Files.newInputStream(path))
+                .filter(Element.class::isInstance)
+                .map(Element.class::cast)
+                .filter(element -> element.getTags().containsKey("building"))
+                .map(Element::getGeometry)
+                .filter(Predicates.notNull())
+                .forEach(collection::add);
+
+        var unionedGeometry = new CascadedPolygonUnion(new CollectionAdapter(collection)).union();
+
+        var unionGeometries = IntStream.range(0, unionedGeometry.getNumGeometries())
+                .mapToObj(unionedGeometry::getGeometryN)
+                .toList();
+
+        System.out.println(unionGeometries.size());
+
+        FileUtils.deleteRecursively(cacheDir);
+
+        logger.info("Finished importing {} into {}", file, database);
+    }
+
+    public static class PolygonUnionConsumer implements Consumer<Element> {
+
+        private final Map<Map<String, Object>, Collection<Element>> groups = new ConcurrentHashMap<>();
+
+        private final Supplier<Collection<Element>> collectionSupplier;
+
+        private final Predicate<Element> filter;
+
+        private final List<String> groupBy;
+
+        public PolygonUnionConsumer(Supplier<Collection<Element>> collectionSupplier, Predicate<Element> filter, List<String> groupBy) {
+            this.collectionSupplier = collectionSupplier;
+            this.filter = filter;
+            this.groupBy = groupBy;
+        }
+
+        @Override
+        public void accept(Element element) {
+            if (filter.test(element)) {
+                var key = groupBy.stream().collect(Collectors.toMap(Function.identity(), element.getTags()::get));
+                var collection = groups.computeIfAbsent(key, k -> collectionSupplier.get());
+                collection.add(element);
+            }
+        }
+
+        public Stream<Geometry> geometries() {
+            return groups.entrySet().stream()
+                    .flatMap(entry -> {
+                        //var tags = entry.getKey();
+                        var collection = entry.getValue();
+                        var geometry = new CascadedPolygonUnion(collection).union();
+                        return IntStream.range(0, geometry.getNumGeometries())
+                                .mapToObj(geometry::getGeometryN);
+                    });
+        }
+    }}
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/UngzipFile.java b/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/UngzipFile.java
index ceea7b39..027cbf8f 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/UngzipFile.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/UngzipFile.java
@@ -20,19 +20,20 @@ import org.slf4j.LoggerFactory;
 
 import java.io.BufferedInputStream;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.file.StandardCopyOption;
 import java.util.zip.GZIPInputStream;
 
-public record UngzipFile(String file, String directory) implements Task {
+public record UngzipFile(Path file, Path directory) implements Task {
 
   private static final Logger logger = LoggerFactory.getLogger(UngzipFile.class);
 
   @Override
   public void execute(WorkflowContext context) throws Exception {
     logger.info("Unzipping {} to {}", file, directory);
-    var filePath = Paths.get(file);
-    var directoryPath = Paths.get(directory);
+    var filePath = file.toAbsolutePath();
+    var directoryPath = directory.toAbsolutePath();
     try (var zis = new GZIPInputStream(new BufferedInputStream(Files.newInputStream(filePath)))) {
       var file = directoryPath.resolve(filePath.getFileName().toString().substring(0, filePath.getFileName().toString().length() - 3));
       Files.copy(zis, file, StandardCopyOption.REPLACE_EXISTING);
diff --git a/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/UnzipFile.java b/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/UnzipFile.java
index ee14a095..3bef1cc9 100644
--- a/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/UnzipFile.java
+++ b/baremaps-core/src/main/java/org/apache/baremaps/workflow/tasks/UnzipFile.java
@@ -17,10 +17,7 @@ import org.apache.baremaps.workflow.WorkflowContext;
 import org.apache.baremaps.workflow.WorkflowException;
 
 import java.io.*;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.nio.file.StandardCopyOption;
-import java.nio.file.StandardOpenOption;
+import java.nio.file.*;
 import java.util.Enumeration;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
@@ -29,7 +26,7 @@ import java.util.zip.ZipInputStream;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public record UnzipFile(String file, String directory) implements Task {
+public record UnzipFile(Path file, Path directory) implements Task {
 
   private static final long THRESHOLD_ENTRIES = 10000;
   private static final long THRESHOLD_SIZE = 10l << 30;
@@ -41,8 +38,8 @@ public record UnzipFile(String file, String directory) implements Task {
   public void execute(WorkflowContext context) throws Exception {
     logger.info("Unzipping {} to {}", file, directory);
 
-    var filePath = Paths.get(file).toAbsolutePath();
-    var directoryPath = Paths.get(directory).toAbsolutePath();
+    var filePath = file.toAbsolutePath();
+    var directoryPath = directory.toAbsolutePath();
 
     try(var zipFile = new ZipFile(filePath.toFile())) {
       var entries = zipFile.entries();
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/database/database/PostgresNodeRepositoryTest.java b/baremaps-core/src/test/java/org/apache/baremaps/database/database/PostgresNodeRepositoryTest.java
index 5b37822d..92397235 100644
--- a/baremaps-core/src/test/java/org/apache/baremaps/database/database/PostgresNodeRepositoryTest.java
+++ b/baremaps-core/src/test/java/org/apache/baremaps/database/database/PostgresNodeRepositoryTest.java
@@ -41,7 +41,7 @@ class PostgresNodeRepositoryTest extends DatabaseContainerTest {
   @Tag("integration")
   void insert() throws RepositoryException {
     nodeRepository.put(Constants.NODE_0);
-    assertEquals(Constants.NODE_0, nodeRepository.get(Constants.NODE_0.getId()));
+    assertEquals(Constants.NODE_0, nodeRepository.get(Constants.NODE_0.id()));
   }
 
   @Test
@@ -50,15 +50,15 @@ class PostgresNodeRepositoryTest extends DatabaseContainerTest {
     List<Node> nodes = Arrays.asList(Constants.NODE_0, Constants.NODE_1, Constants.NODE_2);
     nodeRepository.put(nodes);
     assertIterableEquals(nodes,
-        nodeRepository.get(nodes.stream().map(e -> e.getId()).collect(Collectors.toList())));
+        nodeRepository.get(nodes.stream().map(e -> e.id()).collect(Collectors.toList())));
   }
 
   @Test
   @Tag("integration")
   void delete() throws RepositoryException {
     nodeRepository.put(Constants.NODE_0);
-    nodeRepository.delete(Constants.NODE_0.getId());
-    assertNull(nodeRepository.get(Constants.NODE_0.getId()));
+    nodeRepository.delete(Constants.NODE_0.id());
+    assertNull(nodeRepository.get(Constants.NODE_0.id()));
   }
 
   @Test
@@ -66,9 +66,9 @@ class PostgresNodeRepositoryTest extends DatabaseContainerTest {
   void deleteAll() throws RepositoryException {
     List<Node> nodes = Arrays.asList(Constants.NODE_0, Constants.NODE_1, Constants.NODE_2);
     nodeRepository.put(nodes);
-    nodeRepository.delete(nodes.stream().map(e -> e.getId()).collect(Collectors.toList()));
+    nodeRepository.delete(nodes.stream().map(e -> e.id()).collect(Collectors.toList()));
     assertIterableEquals(Arrays.asList(null, null, null),
-        nodeRepository.get(nodes.stream().map(e -> e.getId()).collect(Collectors.toList())));
+        nodeRepository.get(nodes.stream().map(e -> e.id()).collect(Collectors.toList())));
   }
 
   @Test
@@ -77,6 +77,6 @@ class PostgresNodeRepositoryTest extends DatabaseContainerTest {
     List<Node> nodes = Arrays.asList(Constants.NODE_0, Constants.NODE_1, Constants.NODE_2);
     nodeRepository.copy(nodes);
     assertIterableEquals(nodes,
-        nodeRepository.get(nodes.stream().map(e -> e.getId()).collect(Collectors.toList())));
+        nodeRepository.get(nodes.stream().map(e -> e.id()).collect(Collectors.toList())));
   }
 }
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/database/database/PostgresRelationRepositoryTest.java b/baremaps-core/src/test/java/org/apache/baremaps/database/database/PostgresRelationRepositoryTest.java
index 83fa259d..9b911c29 100644
--- a/baremaps-core/src/test/java/org/apache/baremaps/database/database/PostgresRelationRepositoryTest.java
+++ b/baremaps-core/src/test/java/org/apache/baremaps/database/database/PostgresRelationRepositoryTest.java
@@ -41,7 +41,7 @@ class PostgresRelationRepositoryTest extends DatabaseContainerTest {
   @Tag("integration")
   void insert() throws RepositoryException {
     relationRepository.put(Constants.RELATION_2);
-    assertEquals(Constants.RELATION_2, relationRepository.get(Constants.RELATION_2.getId()));
+    assertEquals(Constants.RELATION_2, relationRepository.get(Constants.RELATION_2.id()));
   }
 
   @Test
@@ -51,15 +51,15 @@ class PostgresRelationRepositoryTest extends DatabaseContainerTest {
         Arrays.asList(Constants.RELATION_2, Constants.RELATION_3, Constants.RELATION_4);
     relationRepository.put(relations);
     assertIterableEquals(relations, relationRepository
-        .get(relations.stream().map(e -> e.getId()).collect(Collectors.toList())));
+        .get(relations.stream().map(e -> e.id()).collect(Collectors.toList())));
   }
 
   @Test
   @Tag("integration")
   void delete() throws RepositoryException {
     relationRepository.put(Constants.RELATION_2);
-    relationRepository.delete(Constants.RELATION_2.getId());
-    assertNull(relationRepository.get(Constants.RELATION_2.getId()));
+    relationRepository.delete(Constants.RELATION_2.id());
+    assertNull(relationRepository.get(Constants.RELATION_2.id()));
   }
 
   @Test
@@ -68,9 +68,9 @@ class PostgresRelationRepositoryTest extends DatabaseContainerTest {
     List<Relation> relations =
         Arrays.asList(Constants.RELATION_2, Constants.RELATION_3, Constants.RELATION_4);
     relationRepository.put(relations);
-    relationRepository.delete(relations.stream().map(e -> e.getId()).collect(Collectors.toList()));
+    relationRepository.delete(relations.stream().map(e -> e.id()).collect(Collectors.toList()));
     assertIterableEquals(Arrays.asList(null, null, null), relationRepository
-        .get(relations.stream().map(e -> e.getId()).collect(Collectors.toList())));
+        .get(relations.stream().map(e -> e.id()).collect(Collectors.toList())));
   }
 
   @Test
@@ -80,6 +80,6 @@ class PostgresRelationRepositoryTest extends DatabaseContainerTest {
         Arrays.asList(Constants.RELATION_2, Constants.RELATION_3, Constants.RELATION_4);
     relationRepository.copy(relations);
     assertIterableEquals(relations, relationRepository
-        .get(relations.stream().map(e -> e.getId()).collect(Collectors.toList())));
+        .get(relations.stream().map(e -> e.id()).collect(Collectors.toList())));
   }
 }
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/database/database/PostgresWayRepositoryTest.java b/baremaps-core/src/test/java/org/apache/baremaps/database/database/PostgresWayRepositoryTest.java
index 78940d9a..cdbc012f 100644
--- a/baremaps-core/src/test/java/org/apache/baremaps/database/database/PostgresWayRepositoryTest.java
+++ b/baremaps-core/src/test/java/org/apache/baremaps/database/database/PostgresWayRepositoryTest.java
@@ -41,7 +41,7 @@ class PostgresWayRepositoryTest extends DatabaseContainerTest {
   @Tag("integration")
   void insert() throws RepositoryException {
     wayRepository.put(Constants.WAY_0);
-    assertEquals(Constants.WAY_0, wayRepository.get(Constants.WAY_0.getId()));
+    assertEquals(Constants.WAY_0, wayRepository.get(Constants.WAY_0.id()));
   }
 
   @Test
@@ -50,15 +50,15 @@ class PostgresWayRepositoryTest extends DatabaseContainerTest {
     List<Way> ways = Arrays.asList(Constants.WAY_0, Constants.WAY_1, Constants.WAY_2);
     wayRepository.put(ways);
     assertIterableEquals(ways,
-        wayRepository.get(ways.stream().map(e -> e.getId()).collect(Collectors.toList())));
+        wayRepository.get(ways.stream().map(e -> e.id()).collect(Collectors.toList())));
   }
 
   @Test
   @Tag("integration")
   void delete() throws RepositoryException {
     wayRepository.put(Constants.WAY_0);
-    wayRepository.delete(Constants.WAY_0.getId());
-    assertNull(wayRepository.get(Constants.WAY_0.getId()));
+    wayRepository.delete(Constants.WAY_0.id());
+    assertNull(wayRepository.get(Constants.WAY_0.id()));
   }
 
   @Test
@@ -66,9 +66,9 @@ class PostgresWayRepositoryTest extends DatabaseContainerTest {
   void deleteAll() throws RepositoryException {
     List<Way> ways = Arrays.asList(Constants.WAY_0, Constants.WAY_1, Constants.WAY_2);
     wayRepository.put(ways);
-    wayRepository.delete(ways.stream().map(e -> e.getId()).collect(Collectors.toList()));
+    wayRepository.delete(ways.stream().map(e -> e.id()).collect(Collectors.toList()));
     assertIterableEquals(Arrays.asList(null, null, null),
-        wayRepository.get(ways.stream().map(e -> e.getId()).collect(Collectors.toList())));
+        wayRepository.get(ways.stream().map(e -> e.id()).collect(Collectors.toList())));
   }
 
   @Test
@@ -77,6 +77,6 @@ class PostgresWayRepositoryTest extends DatabaseContainerTest {
     List<Way> ways = Arrays.asList(Constants.WAY_0, Constants.WAY_1, Constants.WAY_2);
     wayRepository.copy(ways);
     assertIterableEquals(ways,
-        wayRepository.get(ways.stream().map(e -> e.getId()).collect(Collectors.toList())));
+        wayRepository.get(ways.stream().map(e -> e.id()).collect(Collectors.toList())));
   }
 }
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/database/database/WayRepositoryTest.java b/baremaps-core/src/test/java/org/apache/baremaps/database/database/WayRepositoryTest.java
index 032f8903..ffa32987 100644
--- a/baremaps-core/src/test/java/org/apache/baremaps/database/database/WayRepositoryTest.java
+++ b/baremaps-core/src/test/java/org/apache/baremaps/database/database/WayRepositoryTest.java
@@ -41,7 +41,7 @@ class WayRepositoryTest extends DatabaseContainerTest {
   @Tag("integration")
   void insert() throws RepositoryException {
     wayRepository.put(Constants.WAY_1);
-    assertEquals(Constants.WAY_1, wayRepository.get(Constants.WAY_1.getId()));
+    assertEquals(Constants.WAY_1, wayRepository.get(Constants.WAY_1.id()));
   }
 
   @Test
@@ -50,15 +50,15 @@ class WayRepositoryTest extends DatabaseContainerTest {
     List<Way> ways = Arrays.asList(Constants.WAY_1, Constants.WAY_2, Constants.WAY_3);
     wayRepository.put(ways);
     assertIterableEquals(ways,
-        wayRepository.get(ways.stream().map(e -> e.getId()).collect(Collectors.toList())));
+        wayRepository.get(ways.stream().map(e -> e.id()).collect(Collectors.toList())));
   }
 
   @Test
   @Tag("integration")
   void delete() throws RepositoryException {
     wayRepository.put(Constants.WAY_1);
-    wayRepository.delete(Constants.WAY_1.getId());
-    assertNull(wayRepository.get(Constants.WAY_1.getId()));
+    wayRepository.delete(Constants.WAY_1.id());
+    assertNull(wayRepository.get(Constants.WAY_1.id()));
   }
 
   @Test
@@ -66,9 +66,9 @@ class WayRepositoryTest extends DatabaseContainerTest {
   void deleteAll() throws RepositoryException {
     List<Way> ways = Arrays.asList(Constants.WAY_1, Constants.WAY_2, Constants.WAY_3);
     wayRepository.put(ways);
-    wayRepository.delete(ways.stream().map(e -> e.getId()).collect(Collectors.toList()));
+    wayRepository.delete(ways.stream().map(e -> e.id()).collect(Collectors.toList()));
     assertIterableEquals(Arrays.asList(null, null, null),
-        wayRepository.get(ways.stream().map(e -> e.getId()).collect(Collectors.toList())));
+        wayRepository.get(ways.stream().map(e -> e.id()).collect(Collectors.toList())));
   }
 
   @Test
@@ -77,6 +77,6 @@ class WayRepositoryTest extends DatabaseContainerTest {
     List<Way> ways = Arrays.asList(Constants.WAY_1, Constants.WAY_2, Constants.WAY_3);
     wayRepository.copy(ways);
     assertIterableEquals(ways,
-        wayRepository.get(ways.stream().map(e -> e.getId()).collect(Collectors.toList())));
+        wayRepository.get(ways.stream().map(e -> e.id()).collect(Collectors.toList())));
   }
 }
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/mvt/ExpressionsTest.java b/baremaps-core/src/test/java/org/apache/baremaps/mvt/ExpressionsTest.java
new file mode 100644
index 00000000..ae7c0f90
--- /dev/null
+++ b/baremaps-core/src/test/java/org/apache/baremaps/mvt/ExpressionsTest.java
@@ -0,0 +1,232 @@
+/*
+ * Licensed 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.mvt;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import org.apache.baremaps.feature.Feature;
+import org.apache.baremaps.feature.FeatureType;
+import org.apache.baremaps.mvt.Expressions.*;
+import org.junit.jupiter.api.Test;
+
+class ExpressionsTest {
+
+  record FeatureMock(Map<String, Object> properties) implements Feature {
+
+  @Override
+  public FeatureType getType() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setProperty(String name, Object value) {
+    properties.put(name, value);
+  }
+
+  @Override
+  public Object getProperty(String name) {
+    return properties.get(name);
+  }
+
+  @Override
+  public Map<String, Object> getProperties() {
+    return properties;
+  }
+
+  }
+
+  @Test
+  public void literal() throws IOException {
+    assertEquals(1, new Literal(1).evaluate(null));
+    assertEquals("value", new Literal("value").evaluate(null));
+  }
+
+  @Test
+  public void at() throws IOException {
+    var literal = new Literal(List.of(0, 1, 2));
+    assertEquals(0, new At(0, literal).evaluate(null));
+    assertEquals(1, new At(1, literal).evaluate(null));
+    assertEquals(2, new At(2, literal).evaluate(null));
+    assertEquals(null, new At(3, literal).evaluate(null));
+    assertEquals(null, new At(-1, literal).evaluate(null));
+  }
+
+  @Test
+  public void get() throws IOException {
+    assertEquals("value",
+        new Get("key").evaluate(new FeatureMock(Map.of("key", "value"))));
+    assertEquals(null, new Get("key").evaluate(new FeatureMock(Map.of())));
+  }
+
+  @Test
+  public void has() throws IOException {
+    assertEquals(true,
+        new Has("key").evaluate(new FeatureMock(Map.of("key", "value"))));
+    assertEquals(false, new Has("key").evaluate(new FeatureMock(Map.of())));
+  }
+
+  @Test
+  public void inList() throws IOException {
+    var literal = new Literal(List.of(0, 1, 2));
+    assertEquals(true, new In(0, literal).evaluate(null));
+    assertEquals(true, new In(1, literal).evaluate(null));
+    assertEquals(true, new In(2, literal).evaluate(null));
+    assertEquals(false, new In(3, literal).evaluate(null));
+  }
+
+  @Test
+  public void inString() throws IOException {
+    var literal = new Literal("foobar");
+    assertEquals(true, new In("foo", literal).evaluate(null));
+    assertEquals(true, new In("bar", literal).evaluate(null));
+    assertEquals(false, new In("baz", literal).evaluate(null));
+  }
+
+  @Test
+  public void indexOfList() throws IOException {
+    var literal = new Literal(List.of(0, 1, 2));
+    assertEquals(0, new IndexOf(0, literal).evaluate(null));
+    assertEquals(1, new IndexOf(1, literal).evaluate(null));
+    assertEquals(2, new IndexOf(2, literal).evaluate(null));
+    assertEquals(-1, new IndexOf(3, literal).evaluate(null));
+  }
+
+  @Test
+  public void indexOfString() throws IOException {
+    var literal = new Literal("foobar");
+    assertEquals(0, new IndexOf("foo", literal).evaluate(null));
+    assertEquals(3, new IndexOf("bar", literal).evaluate(null));
+    assertEquals(-1, new IndexOf("baz", literal).evaluate(null));
+  }
+
+  @Test
+  public void lengthList() throws IOException {
+    var literal = new Literal(List.of(0, 1, 2));
+    assertEquals(3, new Length(literal).evaluate(null));
+  }
+
+  @Test
+  public void lengthString() throws IOException {
+    var literal = new Literal("foo");
+    assertEquals(3, new Length(literal).evaluate(null));
+  }
+
+  @Test
+  public void lengthNull() throws IOException {
+    var literal = new Literal(null);
+    assertEquals(-1, new Length(literal).evaluate(null));
+  }
+
+  @Test
+  public void slice() throws IOException {
+    var literal = new Literal("foobar");
+    assertEquals("foobar", new Slice(literal, new Literal(0)).evaluate(null));
+    assertEquals("bar", new Slice(literal, new Literal(3)).evaluate(null));
+    assertEquals("foo", new Slice(literal, new Literal(0), new Literal(3)).evaluate(null));
+    assertEquals("bar", new Slice(literal, new Literal(3), new Literal(6)).evaluate(null));
+  }
+
+  @Test
+  public void not() throws IOException {
+    assertEquals(true, Expressions.read("[\"!\", false]").evaluate(null));
+    assertEquals(false, Expressions.read("[\"!\", true]").evaluate(null));
+  }
+
+  @Test
+  public void notEqual() throws IOException {
+    assertEquals(true, Expressions.read("[\"!=\", 1, 2]").evaluate(null));
+    assertEquals(false, Expressions.read("[\"!=\", 1, 1]").evaluate(null));
+  }
+
+  @Test
+  public void less() throws IOException {
+    assertEquals(true, Expressions.read("[\"<\", 1, 2]").evaluate(null));
+    assertEquals(false, Expressions.read("[\"<\", 1, 1]").evaluate(null));
+    assertEquals(false, Expressions.read("[\"<\", 1, 0]").evaluate(null));
+  }
+
+  @Test
+  public void lessOrEqual() throws IOException {
+    assertEquals(true, Expressions.read("[\"<=\", 1, 2]").evaluate(null));
+    assertEquals(true, Expressions.read("[\"<=\", 1, 1]").evaluate(null));
+    assertEquals(false, Expressions.read("[\"<=\", 1, 0]").evaluate(null));
+  }
+
+  @Test
+  public void equal() throws IOException {
+    assertEquals(true, Expressions.read("[\"==\", 1, 1]").evaluate(null));
+    assertEquals(false, Expressions.read("[\"==\", 1, 2]").evaluate(null));
+  }
+
+  @Test
+  public void greater() throws IOException {
+    assertEquals(true, Expressions.read("[\">\", 1, 0]").evaluate(null));
+    assertEquals(false, Expressions.read("[\">\", 1, 1]").evaluate(null));
+    assertEquals(false, Expressions.read("[\">\", 1, 2]").evaluate(null));
+  }
+
+  @Test
+  public void greaterOrEqual() throws IOException {
+    assertEquals(true, Expressions.read("[\">=\", 1, 0]").evaluate(null));
+    assertEquals(true, Expressions.read("[\">=\", 1, 1]").evaluate(null));
+    assertEquals(false, Expressions.read("[\">=\", 1, 2]").evaluate(null));
+  }
+
+  @Test
+  public void all() throws IOException {
+    assertEquals(true, new All(List.of(new Literal(true), new Literal(true))).evaluate(null));
+    assertEquals(false, new All(List.of(new Literal(true), new Literal(false))).evaluate(null));
+    assertEquals(false, new All(List.of(new Literal(false), new Literal(false))).evaluate(null));
+    assertEquals(true, new All(List.of()).evaluate(null));
+  }
+
+  @Test
+  public void any() throws IOException {
+    assertEquals(true, new Any(List.of(new Literal(true), new Literal(true))).evaluate(null));
+    assertEquals(true, new Any(List.of(new Literal(true), new Literal(false))).evaluate(null));
+    assertEquals(false, new Any(List.of(new Literal(false), new Literal(false))).evaluate(null));
+    assertEquals(false, new Any(List.of()).evaluate(null));
+  }
+
+  @Test
+  public void caseExpression() throws IOException {
+    assertEquals("a",
+        new Case(new Literal(true), new Literal("a"), new Literal("b")).evaluate(null));
+    assertEquals("b",
+        new Case(new Literal(false), new Literal("a"), new Literal("b")).evaluate(null));
+  }
+
+  @Test
+  public void coalesce() {
+    assertEquals("a", new Coalesce(List.of(new Literal(null), new Literal("a"), new Literal("b")))
+        .evaluate(null));
+    assertEquals("b", new Coalesce(List.of(new Literal(null), new Literal("b"), new Literal("a")))
+        .evaluate(null));
+    assertEquals(null, new Coalesce(List.of(new Literal(null))).evaluate(null));
+    assertEquals(null, new Coalesce(List.of()).evaluate(null));
+  }
+
+  @Test
+  public void match() throws IOException {
+    assertEquals("foo", Expressions
+        .read("[\"match\", \"foo\", \"foo\", \"foo\", \"bar\", \"bar\", \"baz\"]").evaluate(null));
+    assertEquals("bar", Expressions
+        .read("[\"match\", \"bar\", \"foo\", \"foo\", \"bar\", \"bar\", \"baz\"]").evaluate(null));
+    assertEquals("baz", Expressions
+        .read("[\"match\", \"baz\", \"foo\", \"foo\", \"bar\", \"bar\", \"baz\"]").evaluate(null));
+  }
+
+}
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/openstreetmap/geometry/EntityGeometryBuilderTest.java b/baremaps-core/src/test/java/org/apache/baremaps/openstreetmap/geometry/EntityGeometryBuilderTest.java
index 69c515c4..2b85dc1e 100644
--- a/baremaps-core/src/test/java/org/apache/baremaps/openstreetmap/geometry/EntityGeometryBuilderTest.java
+++ b/baremaps-core/src/test/java/org/apache/baremaps/openstreetmap/geometry/EntityGeometryBuilderTest.java
@@ -111,7 +111,7 @@ class EntityGeometryBuilderTest {
       .asList(NODE_0, NODE_1, NODE_2, NODE_3, NODE_4, NODE_5, NODE_6, NODE_7, NODE_8, NODE_9,
           NODE_10, NODE_11, NODE_12, NODE_13, NODE_14, NODE_15)
       .stream()
-      .collect(Collectors.toMap(n -> n.getId(), n -> new Coordinate(n.getLon(), n.getLat()))));
+      .collect(Collectors.toMap(n -> n.id(), n -> new Coordinate(n.getLon(), n.getLat()))));
 
   static final Way WAY_0 = new Way(0, INFO, ImmutableMap.of(), ImmutableList.of());
 
@@ -131,7 +131,7 @@ class EntityGeometryBuilderTest {
 
   static final LongDataMap<List<Long>> REFERENCE_CACHE =
       new MockLongDataMap(Arrays.asList(WAY_0, WAY_1, WAY_2, WAY_3, WAY_4, WAY_5).stream()
-          .collect(Collectors.toMap(w -> w.getId(), w -> w.getNodes())));
+          .collect(Collectors.toMap(w -> w.id(), w -> w.getNodes())));
 
   static final Relation RELATION_0 = new Relation(0, INFO, ImmutableMap.of(), Arrays.asList());
 
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/openstreetmap/geometry/RelationGeometryBuilderTest.java b/baremaps-core/src/test/java/org/apache/baremaps/openstreetmap/geometry/RelationGeometryBuilderTest.java
index 5512d1cd..fe798c59 100644
--- a/baremaps-core/src/test/java/org/apache/baremaps/openstreetmap/geometry/RelationGeometryBuilderTest.java
+++ b/baremaps-core/src/test/java/org/apache/baremaps/openstreetmap/geometry/RelationGeometryBuilderTest.java
@@ -39,10 +39,10 @@ class RelationGeometryBuilderTest {
     List<Entity> entities = new XmlEntityReader().stream(input).toList();
     LongDataMap<Coordinate> coordinateMap = new MockLongDataMap<>(
         entities.stream().filter(e -> e instanceof Node).map(e -> (Node) e).collect(
-            Collectors.toMap(n -> n.getId(), n -> new Coordinate(n.getLon(), n.getLat()))));
+            Collectors.toMap(n -> n.id(), n -> new Coordinate(n.getLon(), n.getLat()))));
     LongDataMap<List<Long>> referenceMap =
         new MockLongDataMap<>(entities.stream().filter(e -> e instanceof Way).map(e -> (Way) e)
-            .collect(Collectors.toMap(w -> w.getId(), w -> w.getNodes())));
+            .collect(Collectors.toMap(w -> w.id(), w -> w.getNodes())));
     Relation relation = entities.stream().filter(e -> e instanceof Relation).map(e -> (Relation) e)
         .findFirst().get();
     new RelationGeometryBuilder(coordinateMap, referenceMap).accept(relation);
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/testing/PostgresContainerTest.java b/baremaps-core/src/test/java/org/apache/baremaps/testing/PostgresContainerTest.java
index 4536186f..14f8ced7 100644
--- a/baremaps-core/src/test/java/org/apache/baremaps/testing/PostgresContainerTest.java
+++ b/baremaps-core/src/test/java/org/apache/baremaps/testing/PostgresContainerTest.java
@@ -32,7 +32,7 @@ public abstract class PostgresContainerTest {
   public void startContainer() {
     // start the container
     var postgis =
-        DockerImageName.parse("postgis/postgis:13-3.1").asCompatibleSubstituteFor("postgres");
+        DockerImageName.parse("postgis/postgis:14-3.3").asCompatibleSubstituteFor("postgres");
     container = new PostgreSQLContainer(postgis);
     container.start();
 
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/workflow/ObjectMapperTest.java b/baremaps-core/src/test/java/org/apache/baremaps/workflow/ObjectMapperTest.java
index cc72d03f..36f653ad 100644
--- a/baremaps-core/src/test/java/org/apache/baremaps/workflow/ObjectMapperTest.java
+++ b/baremaps-core/src/test/java/org/apache/baremaps/workflow/ObjectMapperTest.java
@@ -16,6 +16,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import java.io.IOException;
+import java.nio.file.Paths;
 import java.util.List;
 import org.apache.baremaps.workflow.tasks.DownloadUrl;
 import org.apache.baremaps.workflow.tasks.ImportOpenStreetMap;
@@ -33,9 +34,9 @@ public class ObjectMapperTest {
             new Step("download", List.of(),
                 List.of(new DownloadUrl(
                     "https://download.geofabrik.de/europe/liechtenstein-latest.osm.pbf",
-                    "liechtenstein-latest.osm.pbf"))),
+                    Paths.get("liechtenstein-latest.osm.pbf")))),
             new Step("import", List.of("download"),
-                List.of(new ImportOpenStreetMap("liechtenstein-latest.osm.pbf",
+                List.of(new ImportOpenStreetMap(Paths.get("liechtenstein-latest.osm.pbf"),
                     "jdbc:postgresql://localhost:5432/baremaps?&user=baremaps&password=baremaps",
                     3857)))));
     var json = mapper.writeValueAsString(workflow1);
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/workflow/WorkflowTest.java b/baremaps-core/src/test/java/org/apache/baremaps/workflow/WorkflowTest.java
index 098ad74a..57bb1fb1 100644
--- a/baremaps-core/src/test/java/org/apache/baremaps/workflow/WorkflowTest.java
+++ b/baremaps-core/src/test/java/org/apache/baremaps/workflow/WorkflowTest.java
@@ -14,6 +14,7 @@ package org.apache.baremaps.workflow;
 
 
 
+import java.nio.file.Paths;
 import java.util.List;
 import org.apache.baremaps.testing.PostgresContainerTest;
 import org.apache.baremaps.workflow.tasks.DownloadUrl;
@@ -31,9 +32,11 @@ class WorkflowTest extends PostgresContainerTest {
   void naturalearthGeoPackage() {
     var workflow = new Workflow(List.of(new Step("fetch-geopackage", List.of(), List.of(
         new DownloadUrl("https://naciscdn.org/naturalearth/packages/natural_earth_vector.gpkg.zip",
-            "natural_earth_vector.gpkg.zip"),
-        new UnzipFile("natural_earth_vector.gpkg.zip", "natural_earth_vector"),
-        new ImportGeoPackage("natural_earth_vector/packages/natural_earth_vector.gpkg", jdbcUrl(),
+            Paths.get("natural_earth_vector.gpkg.zip")),
+        new UnzipFile(Paths.get("natural_earth_vector.gpkg.zip"),
+            Paths.get("natural_earth_vector")),
+        new ImportGeoPackage(Paths.get("natural_earth_vector/packages/natural_earth_vector.gpkg"),
+            jdbcUrl(),
             4326, 3857)))));
     new WorkflowExecutor(workflow).execute().join();
   }
@@ -44,9 +47,11 @@ class WorkflowTest extends PostgresContainerTest {
     var workflow = new Workflow(List.of(new Step("fetch-geopackage", List.of(),
         List.of(
             new DownloadUrl("https://osmdata.openstreetmap.de/download/coastlines-split-4326.zip",
-                "coastlines-split-4326.zip"),
-            new UnzipFile("coastlines-split-4326.zip", "coastlines-split-4326"),
-            new ImportShapefile("coastlines-split-4326/coastlines-split-4326/lines.shp", jdbcUrl(),
+                Paths.get("coastlines-split-4326.zip")),
+            new UnzipFile(Paths.get("coastlines-split-4326.zip"),
+                Paths.get("coastlines-split-4326")),
+            new ImportShapefile(Paths.get("coastlines-split-4326/coastlines-split-4326/lines.shp"),
+                jdbcUrl(),
                 4326, 3857)))));
     new WorkflowExecutor(workflow).execute().join();
   }
@@ -62,7 +67,8 @@ class WorkflowTest extends PostgresContainerTest {
          * "simplified-water-polygons-split-3857.zip", "simplified-water-polygons-split-3857"),
          */
         new ImportShapefile(
-            "simplified-water-polygons-split-3857/simplified-water-polygons-split-3857/simplified_water_polygons.shp",
+            Paths.get(
+                "simplified-water-polygons-split-3857/simplified-water-polygons-split-3857/simplified_water_polygons.shp"),
             "jdbc:postgresql://localhost:5432/baremaps?&user=baremaps&password=baremaps", 3857,
             3857)))));
     new WorkflowExecutor(workflow).execute().join();
@@ -73,8 +79,8 @@ class WorkflowTest extends PostgresContainerTest {
   void workflow() {
     var workflow = new Workflow(List.of(new Step("fetch-geopackage", List.of(), List.of(
         new DownloadUrl("https://naciscdn.org/naturalearth/packages/natural_earth_vector.gpkg.zip",
-            "downloads/import_db.gpkg"),
-        new ImportShapefile("downloads/import_db.gpkg", jdbcUrl(), 4326, 3857)))));
+            Paths.get("downloads/import_db.gpkg")),
+        new ImportShapefile(Paths.get("downloads/import_db.gpkg"), jdbcUrl(), 4326, 3857)))));
     new WorkflowExecutor(workflow).execute().join();
   }
 
@@ -84,26 +90,31 @@ class WorkflowTest extends PostgresContainerTest {
     var workflow = new Workflow(List.of(
         new Step("fetch-geopackage", List.of(),
             List.of(new DownloadUrl("https://tiles.baremaps.com/samples/import_db.gpkg",
-                "downloads/import_db.gpkg"))),
+                Paths.get("downloads/import_db.gpkg")))),
         new Step("import-geopackage", List.of("fetch-geopackage"),
-            List.of(new ImportGeoPackage("downloads/import_db.gpkg", jdbcUrl(), 4326, 3857))),
+            List.of(new ImportGeoPackage(Paths.get("downloads/import_db.gpkg"), jdbcUrl(), 4326,
+                3857))),
         new Step("fetch-osmpbf", List.of(),
             List.of(new DownloadUrl("https://tiles.baremaps.com/samples/liechtenstein.osm.pbf",
-                "downloads/liechtenstein.osm.pbf"))),
+                Paths.get("downloads/liechtenstein.osm.pbf")))),
         new Step("import-osmpbf", List.of("fetch-osmpbf"),
-            List.of(new ImportOpenStreetMap("downloads/liechtenstein.osm.pbf", jdbcUrl(), 3857))),
+            List.of(new ImportOpenStreetMap(Paths.get("downloads/liechtenstein.osm.pbf"), jdbcUrl(),
+                3857))),
         new Step("fetch-shapefile", List.of(), List.of(new DownloadUrl(
             "https://osmdata.openstreetmap.de/download/simplified-water-polygons-split-3857.zip",
-            "downloads/simplified-water-polygons-split-3857.zip"))),
+            Paths.get("downloads/simplified-water-polygons-split-3857.zip")))),
         new Step("unzip-shapefile", List.of("fetch-shapefile"),
             List.of(
-                new UnzipFile("downloads/simplified-water-polygons-split-3857.zip", "archives"))),
+                new UnzipFile(Paths.get("downloads/simplified-water-polygons-split-3857.zip"),
+                    Paths.get("archives")))),
         new Step("fetch-projection", List.of("unzip-shapefile"),
             List.of(new DownloadUrl("https://spatialreference.org/ref/sr-org/epsg3857/prj/",
-                "archives/simplified-water-polygons-split-3857/simplified_water_polygons.prj"))),
+                Paths.get(
+                    "archives/simplified-water-polygons-split-3857/simplified_water_polygons.prj")))),
         new Step("import-shapefile", List.of("fetch-projection"),
             List.of(new ImportShapefile(
-                "archives/simplified-water-polygons-split-3857/simplified_water_polygons.shp",
+                Paths.get(
+                    "archives/simplified-water-polygons-split-3857/simplified_water_polygons.shp"),
                 jdbcUrl(), 3857, 3857)))));
     new WorkflowExecutor(workflow).execute().join();
   }
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/DownloadUrlTest.java b/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/DownloadUrlTest.java
index 1164f192..570d5891 100644
--- a/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/DownloadUrlTest.java
+++ b/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/DownloadUrlTest.java
@@ -29,7 +29,7 @@ class DownloadUrlTest {
     var file = File.createTempFile("test", ".tmp");
     file.deleteOnExit();
     var task = new DownloadUrl("https://raw.githubusercontent.com/baremaps/baremaps/main/README.md",
-        file.getAbsolutePath());
+        file.toPath());
     task.execute(new WorkflowContext());
     assertTrue(Files.readString(file.toPath()).contains("Baremaps"));
   }
@@ -40,7 +40,7 @@ class DownloadUrlTest {
     var directory = Files.createTempDirectory("tmp_");
     var file = directory.resolve("README.md");
     var task = new DownloadUrl("https://raw.githubusercontent.com/baremaps/baremaps/main/README.md",
-        file.toAbsolutePath().toString());
+        file.toAbsolutePath());
     task.execute(new WorkflowContext());
     assertTrue(Files.readString(file).contains("Baremaps"));
     FileUtils.deleteRecursively(directory);
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/ExecuteSqlFileTest.java b/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/ExecuteSqlFileTest.java
index 36ecde0d..93d4274a 100644
--- a/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/ExecuteSqlFileTest.java
+++ b/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/ExecuteSqlFileTest.java
@@ -26,7 +26,7 @@ class ExecuteSqlFileTest extends PostgresContainerTest {
   @Tag("integration")
   void execute() throws Exception {
     var task =
-        new ExecuteSql(jdbcUrl(), TestFiles.resolve("queries/queries.sql").toString(), false);
+        new ExecuteSql(jdbcUrl(), TestFiles.resolve("queries/queries.sql"), false);
     task.execute(new WorkflowContext());
   }
 }
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/ImportGeoPackageTest.java b/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/ImportGeoPackageTest.java
index 0c4db3ef..2c896528 100644
--- a/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/ImportGeoPackageTest.java
+++ b/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/ImportGeoPackageTest.java
@@ -26,7 +26,7 @@ class ImportGeoPackageTest extends PostgresContainerTest {
   @Tag("integration")
   void execute() throws Exception {
     var task =
-        new ImportGeoPackage(TestFiles.resolve("data.gpkg").toString(), jdbcUrl(), 4326, 3857);
+        new ImportGeoPackage(TestFiles.resolve("data.gpkg"), jdbcUrl(), 4326, 3857);
     task.execute(new WorkflowContext());
   }
 }
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/ImportOpenStreetMapTest.java b/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/ImportOpenStreetMapTest.java
index 297811da..00098e63 100644
--- a/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/ImportOpenStreetMapTest.java
+++ b/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/ImportOpenStreetMapTest.java
@@ -26,7 +26,7 @@ class ImportOpenStreetMapTest extends PostgresContainerTest {
   @Tag("integration")
   void execute() throws Exception {
     var task =
-        new ImportOpenStreetMap(TestFiles.resolve("data.osm.pbf").toString(), jdbcUrl(), 3857);
+        new ImportOpenStreetMap(TestFiles.resolve("data.osm.pbf"), jdbcUrl(), 3857);
     task.execute(new WorkflowContext());
   }
 }
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/ImportShapefileTest.java b/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/ImportShapefileTest.java
index dc613c21..7f1a9811 100644
--- a/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/ImportShapefileTest.java
+++ b/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/ImportShapefileTest.java
@@ -29,9 +29,9 @@ class ImportShapefileTest extends PostgresContainerTest {
   void execute() throws Exception {
     var zip = TestFiles.resolve("monaco-shapefile.zip");
     var directory = Files.createTempDirectory("tmp_");
-    var unzip = new UnzipFile(zip.toString(), directory.toString());
+    var unzip = new UnzipFile(zip, directory);
     unzip.execute(new WorkflowContext());
-    var task = new ImportShapefile(directory.resolve("gis_osm_buildings_a_free_1.shp").toString(),
+    var task = new ImportShapefile(directory.resolve("gis_osm_buildings_a_free_1.shp"),
         jdbcUrl(), 4326, 3857);
     task.execute(new WorkflowContext());
     FileUtils.deleteRecursively(directory);
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/ExecuteSqlFileTest.java b/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/SimplifyOpenStreetMapTest.java
similarity index 74%
copy from baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/ExecuteSqlFileTest.java
copy to baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/SimplifyOpenStreetMapTest.java
index 36ecde0d..668e721b 100644
--- a/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/ExecuteSqlFileTest.java
+++ b/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/SimplifyOpenStreetMapTest.java
@@ -12,21 +12,18 @@
 
 package org.apache.baremaps.workflow.tasks;
 
+import static org.junit.jupiter.api.Assertions.*;
 
-
-import org.apache.baremaps.testing.PostgresContainerTest;
 import org.apache.baremaps.testing.TestFiles;
 import org.apache.baremaps.workflow.WorkflowContext;
-import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 
-class ExecuteSqlFileTest extends PostgresContainerTest {
+class SimplifyOpenStreetMapTest {
 
   @Test
-  @Tag("integration")
   void execute() throws Exception {
-    var task =
-        new ExecuteSql(jdbcUrl(), TestFiles.resolve("queries/queries.sql").toString(), false);
+    var task = new SimplifyOpenStreetMap(TestFiles.resolve("liechtenstein/liechtenstein.osm.pbf"),
+        "jdbcUrl()", 3857);
     task.execute(new WorkflowContext());
   }
 }
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/UngzipFileTest.java b/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/UngzipFileTest.java
index ef0934d5..e9612753 100644
--- a/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/UngzipFileTest.java
+++ b/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/UngzipFileTest.java
@@ -28,7 +28,7 @@ class UngzipFileTest {
   void run() throws Exception {
     var gzip = TestFiles.resolve("ripe/sample.txt.gz");
     var directory = Files.createTempDirectory("tmp_");
-    var task = new UngzipFile(gzip.toString(), directory.toString());
+    var task = new UngzipFile(gzip, directory);
     task.execute(new WorkflowContext());
     FileUtils.deleteRecursively(directory);
   }
diff --git a/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/UnzipFileTest.java b/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/UnzipFileTest.java
index 2d396677..59d37c8b 100644
--- a/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/UnzipFileTest.java
+++ b/baremaps-core/src/test/java/org/apache/baremaps/workflow/tasks/UnzipFileTest.java
@@ -28,7 +28,7 @@ class UnzipFileTest {
   void execute() throws Exception {
     var zip = TestFiles.resolve("monaco-shapefile.zip");
     var directory = Files.createTempDirectory("tmp_");
-    var task = new UnzipFile(zip.toString(), directory.toString());
+    var task = new UnzipFile(zip, directory);
     task.execute(new WorkflowContext());
     FileUtils.deleteRecursively(directory);
   }
diff --git a/baremaps-server/src/main/java/org/apache/baremaps/server/DevResources.java b/baremaps-server/src/main/java/org/apache/baremaps/server/DevResources.java
index b749a9f7..b08c6f7e 100644
--- a/baremaps-server/src/main/java/org/apache/baremaps/server/DevResources.java
+++ b/baremaps-server/src/main/java/org/apache/baremaps/server/DevResources.java
@@ -41,8 +41,8 @@ import javax.ws.rs.sse.SseEventSink;
 import org.apache.baremaps.database.tile.PostgresTileStore;
 import org.apache.baremaps.database.tile.Tile;
 import org.apache.baremaps.database.tile.TileStore;
-import org.apache.baremaps.style.Style;
-import org.apache.baremaps.tileset.Tileset;
+import org.apache.baremaps.mvt.style.Style;
+import org.apache.baremaps.mvt.tileset.Tileset;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
diff --git a/baremaps-server/src/main/java/org/apache/baremaps/server/ServerResources.java b/baremaps-server/src/main/java/org/apache/baremaps/server/ServerResources.java
index 08990b7c..0300046d 100644
--- a/baremaps-server/src/main/java/org/apache/baremaps/server/ServerResources.java
+++ b/baremaps-server/src/main/java/org/apache/baremaps/server/ServerResources.java
@@ -32,9 +32,9 @@ import javax.ws.rs.core.Response;
 import org.apache.baremaps.database.tile.Tile;
 import org.apache.baremaps.database.tile.TileStore;
 import org.apache.baremaps.database.tile.TileStoreException;
-import org.apache.baremaps.style.Style;
-import org.apache.baremaps.tileset.Tileset;
-import org.apache.baremaps.tileset.TilesetLayer;
+import org.apache.baremaps.mvt.style.Style;
+import org.apache.baremaps.mvt.tileset.Tileset;
+import org.apache.baremaps.mvt.tileset.TilesetLayer;
 
 @Singleton
 @javax.ws.rs.Path("/")
diff --git a/pom.xml b/pom.xml
index ca1e9894..e31f8554 100644
--- a/pom.xml
+++ b/pom.xml
@@ -71,7 +71,7 @@
     <version.lib.caffeine>3.1.1</version.lib.caffeine>
     <version.lib.commons-compress>1.21</version.lib.commons-compress>
     <version.lib.fastutil>8.5.9</version.lib.fastutil>
-    <version.lib.geoapi>4.0-M19</version.lib.geoapi>
+    <version.lib.flatgeobuf>3.24.0</version.lib.flatgeobuf>
     <version.lib.geopackage>6.5.0</version.lib.geopackage>
     <version.lib.graalvm>22.2.0</version.lib.graalvm>
     <version.lib.guava>31.1-jre</version.lib.guava>
@@ -95,7 +95,6 @@
     <version.lib.protobuf>3.21.6</version.lib.protobuf>
     <version.lib.servicetalk>0.42.18</version.lib.servicetalk>
     <version.lib.servlet>3.1.0</version.lib.servlet>
-    <version.lib.sis>2.0-M0117</version.lib.sis>
     <version.lib.slf4j>2.0.3</version.lib.slf4j>
     <version.lib.sqlite>3.39.3.0</version.lib.sqlite>
     <version.lib.swagger>1.6.3</version.lib.swagger>
@@ -353,51 +352,6 @@
         <artifactId>lucene-spatial</artifactId>
         <version>${version.lucene-spatial}</version>
       </dependency>
-      <dependency>
-        <groupId>org.apache.sis.core</groupId>
-        <artifactId>sis-feature</artifactId>
-        <version>${version.lib.sis}</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.sis.core</groupId>
-        <artifactId>sis-referencing</artifactId>
-        <version>${version.lib.sis}</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.sis.core</groupId>
-        <artifactId>sis-utility</artifactId>
-        <version>${version.lib.sis}</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.sis.storage</groupId>
-        <artifactId>sis-earth-observation</artifactId>
-        <version>${version.lib.sis}</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.sis.storage</groupId>
-        <artifactId>sis-geotiff</artifactId>
-        <version>${version.lib.sis}</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.sis.storage</groupId>
-        <artifactId>sis-netcdf</artifactId>
-        <version>${version.lib.sis}</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.sis.storage</groupId>
-        <artifactId>sis-sqlstore</artifactId>
-        <version>${version.lib.sis}</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.sis.storage</groupId>
-        <artifactId>sis-storage</artifactId>
-        <version>${version.lib.sis}</version>
-      </dependency>
-      <dependency>
-        <groupId>org.apache.sis.storage</groupId>
-        <artifactId>sis-xmlstore</artifactId>
-        <version>${version.lib.sis}</version>
-      </dependency>
       <dependency>
         <groupId>org.awaitility</groupId>
         <artifactId>awaitility</artifactId>
@@ -511,6 +465,11 @@
         <version>${version.lib.testcontainers}</version>
         <scope>test</scope>
       </dependency>
+      <dependency>
+        <groupId>org.wololo</groupId>
+        <artifactId>flatgeobuf</artifactId>
+        <version>${version.lib.flatgeobuf}</version>
+      </dependency>
       <dependency>
         <groupId>org.xerial</groupId>
         <artifactId>sqlite-jdbc</artifactId>
@@ -525,6 +484,22 @@
   </dependencyManagement>
 
   <dependencies>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-annotations</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.datatype</groupId>
+      <artifactId>jackson-datatype-jdk8</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-api</artifactId>
@@ -565,6 +540,10 @@
       <groupId>org.testcontainers</groupId>
       <artifactId>testcontainers</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.wololo</groupId>
+      <artifactId>flatgeobuf</artifactId>
+    </dependency>
   </dependencies>
 
   <repositories>