xmax,
+ List ymax
+ ) {}
+
+ public record Covering(
+ CoveringBbox bbox
+ ) {}
+
+
+ public static GeoParquetMetadata parse(FileMetaData metadata) throws IOException {
+ String string = metadata.getKeyValueMetaData().get("geo");
+ if (string != null) {
+ try {
+ return mapper.readValue(string, GeoParquetMetadata.class);
+ } catch (JsonProcessingException e) {
+ LOGGER.warn("Invalid geoparquet metadata", e);
+ }
+ }
+ // fallback
+ for (var field : metadata.getSchema().asGroupType().getFields()) {
+ if (field.isPrimitive() &&
+ field.asPrimitiveType().getPrimitiveTypeName() == PrimitiveType.PrimitiveTypeName.BINARY) {
+ switch (field.getName()) {
+ case "geometry", "wkb_geometry" -> {
+ return new GeoParquetMetadata("1.0.0", field.getName(), Map.of(
+ field.getName(), new ColumnMetadata("WKB")));
+ }
+ case "wkt_geometry" -> {
+ return new GeoParquetMetadata("1.0.0", field.getName(), Map.of(
+ field.getName(), new ColumnMetadata("WKT")));
+ }
+ default -> {
+ //ignore
+ }
+ }
+ }
+ }
+ throw new IOException(
+ "No valid geometry columns found: " + metadata.getSchema().asGroupType().getFields().stream().map(
+ Type::getName).toList());
+ }
+}
diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/parquet/Interval.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/parquet/Interval.java
new file mode 100644
index 0000000000..f1cb4d464b
--- /dev/null
+++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/parquet/Interval.java
@@ -0,0 +1,42 @@
+package com.onthegomap.planetiler.reader.parquet;
+
+import java.time.Duration;
+import java.time.Period;
+import java.time.temporal.Temporal;
+import java.time.temporal.TemporalAmount;
+import java.time.temporal.TemporalUnit;
+import java.util.List;
+import java.util.stream.Stream;
+
+/**
+ * Represents a parquet
+ * interval datatype which has a month, day, and millisecond part.
+ *
+ * Built-in java {@link TemporalAmount} implementations can only store a period or duration amount, but not both.
+ */
+public record Interval(Period period, Duration duration) implements TemporalAmount {
+
+ public static Interval of(int months, long days, long millis) {
+ return new Interval(Period.ofMonths(months).plusDays(days), Duration.ofMillis(millis));
+ }
+
+ @Override
+ public long get(TemporalUnit unit) {
+ return period.get(unit) + duration.get(unit);
+ }
+
+ @Override
+ public List getUnits() {
+ return Stream.concat(period.getUnits().stream(), duration.getUnits().stream()).toList();
+ }
+
+ @Override
+ public Temporal addTo(Temporal temporal) {
+ return temporal.plus(period).plus(duration);
+ }
+
+ @Override
+ public Temporal subtractFrom(Temporal temporal) {
+ return temporal.minus(period).minus(duration);
+ }
+}
diff --git a/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/parquet/MapRecordMaterializer.java b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/parquet/MapRecordMaterializer.java
new file mode 100644
index 0000000000..2de7ee0a75
--- /dev/null
+++ b/planetiler-core/src/main/java/com/onthegomap/planetiler/reader/parquet/MapRecordMaterializer.java
@@ -0,0 +1,515 @@
+package com.onthegomap.planetiler.reader.parquet;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.Period;
+import java.time.ZoneOffset;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.UUID;
+import java.util.function.Function;
+import java.util.function.IntConsumer;
+import java.util.function.LongFunction;
+import java.util.stream.IntStream;
+import org.apache.parquet.column.Dictionary;
+import org.apache.parquet.io.api.Binary;
+import org.apache.parquet.io.api.Converter;
+import org.apache.parquet.io.api.GroupConverter;
+import org.apache.parquet.io.api.PrimitiveConverter;
+import org.apache.parquet.io.api.RecordMaterializer;
+import org.apache.parquet.schema.GroupType;
+import org.apache.parquet.schema.LogicalTypeAnnotation;
+import org.apache.parquet.schema.LogicalTypeAnnotation.DateLogicalTypeAnnotation;
+import org.apache.parquet.schema.LogicalTypeAnnotation.DecimalLogicalTypeAnnotation;
+import org.apache.parquet.schema.LogicalTypeAnnotation.EnumLogicalTypeAnnotation;
+import org.apache.parquet.schema.LogicalTypeAnnotation.IntLogicalTypeAnnotation;
+import org.apache.parquet.schema.LogicalTypeAnnotation.IntervalLogicalTypeAnnotation;
+import org.apache.parquet.schema.LogicalTypeAnnotation.JsonLogicalTypeAnnotation;
+import org.apache.parquet.schema.LogicalTypeAnnotation.StringLogicalTypeAnnotation;
+import org.apache.parquet.schema.LogicalTypeAnnotation.TimeLogicalTypeAnnotation;
+import org.apache.parquet.schema.LogicalTypeAnnotation.TimestampLogicalTypeAnnotation;
+import org.apache.parquet.schema.LogicalTypeAnnotation.UUIDLogicalTypeAnnotation;
+import org.apache.parquet.schema.MessageType;
+import org.apache.parquet.schema.Type;
+
+/**
+ * Simple converter for parquet datatypes that maps all structs to {@code Map} and handles deserializing
+ * list and map nested
+ * types into java {@link List Lists} and {@link Map Maps}.
+ */
+public class MapRecordMaterializer extends RecordMaterializer