Add search service for osm entities
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 05258f1..b791c03 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
@@ -21,6 +21,7 @@
 import io.servicetalk.http.router.jersey.HttpJerseyRouterBuilder;
 import java.nio.file.Path;
 import java.util.concurrent.Callable;
+import javax.sql.DataSource;
 import org.apache.baremaps.cli.Options;
 import org.apache.baremaps.database.PostgresUtils;
 import org.apache.baremaps.database.tile.PostgresTileStore;
@@ -29,6 +30,7 @@
 import org.apache.baremaps.mvt.tileset.Tileset;
 import org.apache.baremaps.server.ConfigReader;
 import org.apache.baremaps.server.CorsFilter;
+import org.apache.baremaps.server.SearchResources;
 import org.apache.baremaps.server.ServerResources;
 import org.glassfish.hk2.utilities.binding.AbstractBinder;
 import org.glassfish.jersey.server.ResourceConfig;
@@ -80,13 +82,18 @@
 
     // Configure the application
     var application =
-        new ResourceConfig().register(CorsFilter.class).register(ServerResources.class)
-            .register(contextResolverFor(objectMapper)).register(new AbstractBinder() {
+        new ResourceConfig()
+            .register(CorsFilter.class)
+            .register(SearchResources.class)
+            .register(ServerResources.class)
+            .register(contextResolverFor(objectMapper))
+            .register(new AbstractBinder() {
               @Override
               protected void configure() {
                 bind(Serve.this.tileset).to(Path.class).named("tileset");
                 bind(style).to(Path.class).named("style");
                 bind(tileCache).to(TileStore.class);
+                bind(datasource).to(DataSource.class);
                 bind(objectMapper).to(ObjectMapper.class);
               }
             });
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 513790e..0806937 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
@@ -20,7 +20,7 @@
 import java.util.HashMap;
 import java.util.Map;
 
-class PostgresJsonbMapper {
+public class PostgresJsonbMapper {
 
   private static final ObjectMapper mapper = new ObjectMapper();
 
diff --git a/baremaps-server/src/main/java/org/apache/baremaps/server/SearchResources.java b/baremaps-server/src/main/java/org/apache/baremaps/server/SearchResources.java
new file mode 100644
index 0000000..ea8e279
--- /dev/null
+++ b/baremaps-server/src/main/java/org/apache/baremaps/server/SearchResources.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.server;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import mil.nga.sf.Geometry;
+import org.apache.baremaps.database.repository.PostgresJsonbMapper;
+import org.apache.baremaps.openstreetmap.utils.GeometryUtils;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.spatial4j.context.SpatialContext;
+import org.locationtech.spatial4j.context.SpatialContextFactory;
+import org.locationtech.spatial4j.io.GeoJSONWriter;
+
+
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import javax.sql.DataSource;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+
+@Singleton
+@Path("/")
+public class SearchResources {
+
+  private final DataSource dataSource;
+
+  private final String searchQuery = """
+          SELECT id, tags, ST_AsGeoJSON(geom) AS geom
+          FROM  osm_named_entities
+          WHERE osm_named_entities.tsv @@ to_tsquery('English', ?)
+          ORDER BY ts_rank_cd(osm_named_entities.tsv, to_tsquery('English', ?)) DESC
+          LIMIT 10
+           """;
+
+  @Inject
+  public SearchResources(DataSource dataSource) {
+    this.dataSource = dataSource;
+  }
+
+  private record Feature(String id, Map<String, Object> properties, JsonNode geometry) {
+
+  }
+
+  @GET
+  @Path("/search")
+  @Produces(MediaType.APPLICATION_JSON)
+  public List<Feature> search(@QueryParam("query") String query) {
+    try (Connection connection = dataSource.getConnection()) {
+      var statement = connection.prepareStatement(searchQuery);
+      statement.setString(1, query);
+      statement.setString(2, query);
+      var resultSet = statement.executeQuery();
+      var results = new ArrayList<Feature>();
+      while (resultSet.next()) {
+        var id = resultSet.getLong("id");
+        var tags = PostgresJsonbMapper.toMap(resultSet.getString("tags"));
+        var json = resultSet.getString("geom");
+        var geometry = new ObjectMapper().readTree(json);
+        var feature = new Feature(String.valueOf(id), tags, geometry);
+        results.add(feature);
+      }
+      return results;
+    } catch (SQLException e) {
+      throw new RuntimeException(e);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+}
diff --git a/basemap/queries/osm_named_entities.sql b/basemap/queries/osm_named_entities.sql
new file mode 100644
index 0000000..89a4830
--- /dev/null
+++ b/basemap/queries/osm_named_entities.sql
@@ -0,0 +1,22 @@
+DROP MATERIALIZED VIEW IF EXISTS osm_named_entities CASCADE;
+
+CREATE MATERIALIZED VIEW osm_named_entities AS
+    SELECT id, 1 AS type, tags AS tags, to_tsvector('English', tags::text) AS tsv, st_envelope(geom) AS geom
+    FROM osm_nodes
+    WHERE tags ? 'name' AND geom IS NOT NULL
+    UNION ALL
+    SELECT id, 2 AS type, tags AS tags, to_tsvector('English', tags::text) AS tsv, st_envelope(geom) AS geom
+    FROM osm_ways
+    WHERE tags ? 'name' AND geom IS NOT NULL
+    UNION ALL
+    SELECT id, 3 AS type, tags AS tags, to_tsvector('English', tags::text) AS tsv, st_envelope(geom) AS geom
+    FROM osm_relations
+    WHERE tags ? 'name' AND geom IS NOT NULL;
+
+CREATE INDEX osm_named_entities_gin
+    ON osm_named_entities
+    USING gin(tsv);
+
+CREATE INDEX osm_named_entities_gist
+    ON osm_named_entities
+    USING gist(geom);
\ No newline at end of file
diff --git a/basemap/queries/osm_nodes_prepare.sql b/basemap/queries/osm_nodes_prepare.sql
index e7bd764..4521603 100644
--- a/basemap/queries/osm_nodes_prepare.sql
+++ b/basemap/queries/osm_nodes_prepare.sql
@@ -1,2 +1,3 @@
 CREATE INDEX IF NOT EXISTS osm_nodes_tags_index ON osm_nodes USING gin (tags);
 CREATE INDEX IF NOT EXISTS osm_nodes_geom_index ON osm_nodes USING spgist (geom);
+CREATE INDEX IF NOT EXISTS osm_nodes_geom_fulltext_index ON osm_nodes USING gist ((to_tsvector('English', tags::text))) ;