Repository: komoot/photon
Pull request #651 (2022-03-03 10:52:32)

Introduce abstract result classes
The second part of encapsulating ElasticSearch code. Introduces an abstract result class, where the ES subpackage implements the necessary accessors to its internal SearchHit class.

With the abstract result, the deduplication and conversion into the GeocodeJSON result can be moved into the generic handler part. The assumption here is that all database backends will use the same data schema. The abstract results just hids how the data is retrieved.


Patch:
diff --git a/src/main/java/de/komoot/photon/ReverseSearchRequestHandler.java b/src/main/java/de/komoot/photon/ReverseSearchRequestHandler.java
index 0cfb19c..ef9fde8 100644
--- a/src/main/java/de/komoot/photon/ReverseSearchRequestHandler.java
+++ b/src/main/java/de/komoot/photon/ReverseSearchRequestHandler.java
@@ -3,8 +3,9 @@ package de.komoot.photon;
 import de.komoot.photon.query.BadRequestException;
 import de.komoot.photon.query.ReverseRequest;
 import de.komoot.photon.query.ReverseRequestFactory;
+import de.komoot.photon.searcher.GeocodeJsonFormatter;
+import de.komoot.photon.searcher.PhotonResult;
 import de.komoot.photon.searcher.ReverseHandler;
-import de.komoot.photon.utils.ConvertToGeoJson;
 import org.json.JSONObject;
 import spark.Request;
 import spark.Response;
@@ -21,13 +22,11 @@ import static spark.Spark.halt;
 public class ReverseSearchRequestHandler extends RouteImpl {
     private final ReverseRequestFactory reverseRequestFactory;
     private final ReverseHandler requestHandler;
-    private final ConvertToGeoJson geoJsonConverter;
 
     ReverseSearchRequestHandler(String path, ReverseHandler dbHandler, String[] languages, String defaultLanguage) {
         super(path);
         List<String> supportedLanguages = Arrays.asList(languages);
         this.reverseRequestFactory = new ReverseRequestFactory(supportedLanguages, defaultLanguage);
-        this.geoJsonConverter = new ConvertToGeoJson();
         this.requestHandler = dbHandler;
     }
 
@@ -41,11 +40,14 @@ public class ReverseSearchRequestHandler extends RouteImpl {
             json.put("message", e.getMessage());
             halt(e.getHttpStatus(), json.toString());
         }
-        List<JSONObject> results = requestHandler.reverse(photonRequest);
-        JSONObject geoJsonResults = geoJsonConverter.convert(results);
-        if (request.queryParams("debug") != null)
-            return geoJsonResults.toString(4);
 
-        return geoJsonResults.toString();
+        List<PhotonResult> results = requestHandler.reverse(photonRequest);
+
+        // Restrict to the requested limit.
+        if (results.size() > photonRequest.getLimit()) {
+            results = results.subList(0, photonRequest.getLimit());
+        }
+
+        return new GeocodeJsonFormatter(false, photonRequest.getLanguage()).convert(results, null);
     }
 }
diff --git a/src/main/java/de/komoot/photon/SearchRequestHandler.java b/src/main/java/de/komoot/photon/SearchRequestHandler.java
index 468b026..227c7fc 100644
--- a/src/main/java/de/komoot/photon/SearchRequestHandler.java
+++ b/src/main/java/de/komoot/photon/SearchRequestHandler.java
@@ -3,8 +3,7 @@ package de.komoot.photon;
 import de.komoot.photon.query.BadRequestException;
 import de.komoot.photon.query.PhotonRequest;
 import de.komoot.photon.query.PhotonRequestFactory;
-import de.komoot.photon.searcher.SearchHandler;
-import de.komoot.photon.utils.ConvertToGeoJson;
+import de.komoot.photon.searcher.*;
 import org.json.JSONObject;
 import spark.Request;
 import spark.Response;
@@ -21,13 +20,11 @@ import static spark.Spark.halt;
 public class SearchRequestHandler extends RouteImpl {
     private final PhotonRequestFactory photonRequestFactory;
     private final SearchHandler requestHandler;
-    private final ConvertToGeoJson geoJsonConverter;
 
     SearchRequestHandler(String path, SearchHandler dbHandler, String[] languages, String defaultLanguage) {
         super(path);
         List<String> supportedLanguages = Arrays.asList(languages);
         this.photonRequestFactory = new PhotonRequestFactory(supportedLanguages, defaultLanguage);
-        this.geoJsonConverter = new ConvertToGeoJson();
         this.requestHandler = dbHandler;
     }
 
@@ -41,15 +38,22 @@ public class SearchRequestHandler extends RouteImpl {
             json.put("message", e.getMessage());
             halt(e.getHttpStatus(), json.toString());
         }
-        List<JSONObject> results = requestHandler.search(photonRequest);
-        JSONObject geoJsonResults = geoJsonConverter.convert(results);
+
+        List<PhotonResult> results = requestHandler.search(photonRequest);
+
+        // Futher filtering
+        results = new StreetDupesRemover(photonRequest.getLanguage()).execute(results);
+
+        // Restrict to the requested limit.
+        if (results.size() > photonRequest.getLimit()) {
+            results = results.subList(0, photonRequest.getLimit());
+        }
+
+        String debugInfo = null;
         if (photonRequest.getDebug()) {
-            JSONObject debug = new JSONObject();
-            debug.put("query", new JSONObject(requestHandler.dumpQuery(photonRequest)));
-            geoJsonResults.put("debug", debug);
-            return geoJsonResults.toString(4);
+            debugInfo = requestHandler.dumpQuery(photonRequest);
         }
 
-        return geoJsonResults.toString();
+        return new GeocodeJsonFormatter(photonRequest.getDebug(), photonRequest.getLanguage()).convert(results, debugInfo);
     }
 }
\ No newline at end of file
diff --git a/src/main/java/de/komoot/photon/elasticsearch/ElasticResult.java b/src/main/java/de/komoot/photon/elasticsearch/ElasticResult.java
new file mode 100644
index 0000000..960b6d7
--- /dev/null
+++ b/src/main/java/de/komoot/photon/elasticsearch/ElasticResult.java
@@ -0,0 +1,82 @@
+package de.komoot.photon.elasticsearch;
+
+import de.komoot.photon.Constants;
+import de.komoot.photon.searcher.PhotonResult;
+import lombok.extern.slf4j.Slf4j;
+import org.elasticsearch.search.SearchHit;
+
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+public class ElasticResult implements PhotonResult {
+    private static final String[] NAME_PRECEDENCE = {"default", "housename", "int", "loc", "reg", "alt", "old"};
+
+    private final SearchHit result;
+
+    ElasticResult(SearchHit result) {
+        this.result = result;
+    }
+
+    @Override
+    public Object get(String key) {
+        return result.getSource().get(key);
+    }
+
+    @Override
+    public String getLocalised(String key, String language) {
+        final Map<String, String> map = (Map<String, String>) result.getSource().get(key);
+        if (map == null) return null;
+
+        if (map.get(language) != null) {
+            // language specific field
+            return map.get(language);
+        }
+
+        if ("name".equals(key)) {
+            for (String name : NAME_PRECEDENCE) {
+                if (map.containsKey(name))
+                    return map.get(name);
+            }
+        }
+
+        return map.get("default");
+    }
+
+    @Override
+    public Map<String, String> getMap(String key) {
+        return (Map<String, String>) result.getSource().get(key);
+    }
+
+    @Override
+    public double[] getCoordinates() {
+        final Map<String, Double> coordinate = (Map<String, Double>) result.getSource().get("coordinate");
+        if (coordinate == null) {
+            log.error(String.format("invalid data [id=%s, type=%s], coordinate is missing!",
+                                    result.getSource().get(Constants.OSM_ID),
+                                    result.getSource().get(Constants.OSM_VALUE)));
+            return new double[]{Double.NaN, Double.NaN};
+        }
+
+        return new double[]{coordinate.get(Constants.LON), coordinate.get(Constants.LAT)};
+    }
+
+    @Override
+    public double[] getExtent() {
+        final Map<String, Object> extent = (Map<String, Object>) result.getSource().get("extent");
+        if (extent == null) {
+            return null;
+        }
+
+        final List<List<Double>> coords = (List<List<Double>>) extent.get("coordinates");
+        final List<Double> nw = coords.get(0);
+        final List<Double> se = coords.get(1);
+
+        return new double[]{nw.get(0), nw.get(1), se.get(0), se.get(1)};
+    }
+
+    @Override
+    public double getScore() {
+        return result.getScore();
+    }
+}
diff --git a/src/main/java/de/komoot/photon/elasticsearch/ElasticsearchReverseHandler.java b/src/main/java/de/komoot/photon/elasticsearch/ElasticsearchReverseHandler.java
index b705b04..e32cce2 100644
--- a/src/main/java/de/komoot/photon/elasticsearch/ElasticsearchReverseHandler.java
+++ b/src/main/java/de/komoot/photon/elasticsearch/ElasticsearchReverseHandler.java
@@ -2,8 +2,8 @@ package de.komoot.photon.elasticsearch;
 
 import com.vividsolutions.jts.geom.Point;
 import de.komoot.photon.query.ReverseRequest;
+import de.komoot.photon.searcher.PhotonResult;
 import de.komoot.photon.searcher.ReverseHandler;
-import de.komoot.photon.utils.ConvertToJson;
 import org.elasticsearch.action.search.SearchRequestBuilder;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.action.search.SearchType;
@@ -11,14 +11,15 @@ import org.elasticsearch.client.Client;
 import org.elasticsearch.common.geo.GeoPoint;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.search.SearchHit;
 import org.elasticsearch.search.sort.SortBuilders;
 import org.elasticsearch.search.sort.SortOrder;
-import org.json.JSONObject;
 
+import java.util.ArrayList;
 import java.util.List;
 
 /**
- * @author svantulden
+ * Execute a reverse lookup on a Elasticsearch database.
  */
 public class ElasticsearchReverseHandler implements ReverseHandler {
     private Client client;
@@ -28,15 +29,17 @@ public class ElasticsearchReverseHandler implements ReverseHandler {
     }
 
     @Override
-    public List<JSONObject> reverse(ReverseRequest photonRequest) {
+    public List<PhotonResult> reverse(ReverseRequest photonRequest) {
         ReverseQueryBuilder queryBuilder = buildQuery(photonRequest);
         SearchResponse results = search(queryBuilder.buildQuery(), photonRequest.getLimit(), photonRequest.getLocation(),
                 photonRequest.getLocationDistanceSort());
-        List<JSONObject> resultJsonObjects = new ConvertToJson(photonRequest.getLanguage()).convert(results, false);
-        if (resultJsonObjects.size() > photonRequest.getLimit()) {
-            resultJsonObjects = resultJsonObjects.subList(0, photonRequest.getLimit());
+
+        List<PhotonResult> ret = new ArrayList<>((int) results.getHits().getTotalHits());
+        for (SearchHit hit : results.getHits()) {
+            ret.add(new ElasticResult(hit));
         }
-        return resultJsonObjects;
+
+        return ret;
     }
 
 
diff --git a/src/main/java/de/komoot/photon/elasticsearch/ElasticsearchSearchHandler.java b/src/main/java/de/komoot/photon/elasticsearch/ElasticsearchSearchHandler.java
index a16a182..111b028 100644
--- a/src/main/java/de/komoot/photon/elasticsearch/ElasticsearchSearchHandler.java
+++ b/src/main/java/de/komoot/photon/elasticsearch/ElasticsearchSearchHandler.java
@@ -1,16 +1,16 @@
 package de.komoot.photon.elasticsearch;
 
 import de.komoot.photon.query.PhotonRequest;
+import de.komoot.photon.searcher.PhotonResult;
 import de.komoot.photon.searcher.SearchHandler;
-import de.komoot.photon.searcher.StreetDupesRemover;
-import de.komoot.photon.utils.ConvertToJson;
 import org.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.action.search.SearchType;
 import org.elasticsearch.client.Client;
 import org.elasticsearch.common.unit.TimeValue;
 import org.elasticsearch.index.query.QueryBuilder;
-import org.json.JSONObject;
+import org.elasticsearch.search.SearchHit;
 
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -27,24 +27,25 @@ public class ElasticsearchSearchHandler implements SearchHandler {
     }
 
     @Override
-    public List<JSONObject> search(PhotonRequest photonRequest) {
-        lastLenient = false;
+    public List<PhotonResult> search(PhotonRequest photonRequest) {
         PhotonQueryBuilder queryBuilder = buildQuery(photonRequest, false);
+
         // for the case of deduplication we need a bit more results, #300
         int limit = photonRequest.getLimit();
         int extLimit = limit > 1 ? (int) Math.round(photonRequest.getLimit() * 1.5) : 1;
+
         SearchResponse results = sendQuery(queryBuilder.buildQuery(), extLimit);
+
         if (results.getHits().getTotalHits() == 0) {
-            lastLenient = true;
             results = sendQuery(buildQuery(photonRequest, true).buildQuery(), extLimit);
         }
-        List<JSONObject> resultJsonObjects = new ConvertToJson(photonRequest.getLanguage()).convert(results, photonRequest.getDebug());
-        StreetDupesRemover streetDupesRemover = new StreetDupesRemover(photonRequest.getLanguage());
-        resultJsonObjects = streetDupesRemover.execute(resultJsonObjects);
-        if (resultJsonObjects.size() > limit) {
-            resultJsonObjects = resultJsonObjects.subList(0, limit);
+
+        List<PhotonResult> ret = new ArrayList<>((int) results.getHits().getTotalHits());
+        for (SearchHit hit : results.getHits()) {
+            ret.add(new ElasticResult(hit));
         }
-        return resultJsonObjects;
+
+        return ret;
     }
 
     public String dumpQuery(PhotonRequest photonRequest) {
@@ -52,6 +53,7 @@ public class ElasticsearchSearchHandler implements SearchHandler {
     }
 
    public PhotonQueryBuilder buildQuery(PhotonRequest photonRequest, boolean lenient) {
+       lastLenient = lenient;
         return PhotonQueryBuilder.
                 builder(photonRequest.getQuery(), photonRequest.getLanguage(), supportedLanguages, lenient).
                 withTags(photonRequest.tags()).
diff --git a/src/main/java/de/komoot/photon/searcher/GeocodeJsonFormatter.java b/src/main/java/de/komoot/photon/searcher/GeocodeJsonFormatter.java
new file mode 100644
index 0000000..a9df3b7
--- /dev/null
+++ b/src/main/java/de/komoot/photon/searcher/GeocodeJsonFormatter.java
@@ -0,0 +1,85 @@
+package de.komoot.photon.searcher;
+
+import de.komoot.photon.Constants;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.List;
+
+/**
+ * Format a database result into a Photon GeocodeJson response.
+ */
+public class GeocodeJsonFormatter implements ResultFormatter {
+    private static final String[] KEYS_LANG_UNSPEC = {Constants.OSM_TYPE, Constants.OSM_ID, Constants.OSM_KEY, Constants.OSM_VALUE, Constants.OBJECT_TYPE, Constants.POSTCODE, Constants.HOUSENUMBER, Constants.COUNTRYCODE};
+    private static final String[] KEYS_LANG_SPEC = {Constants.NAME, Constants.COUNTRY, Constants.CITY, Constants.DISTRICT, Constants.LOCALITY, Constants.STREET, Constants.STATE, Constants.COUNTY};
+    private static final String[] NAME_PRECEDENCE = {"default", "housename", "int", "loc", "reg", "alt", "old"};
+
+    private final boolean addDebugInfo;
+    private final String language;
+
+    public GeocodeJsonFormatter(boolean addDebugInfo, String language) {
+        this.addDebugInfo = addDebugInfo;
+        this.language = language;
+    }
+
+    @Override
+    public String convert(List<PhotonResult> results, String debugInfo) {
+        final JSONArray features = new JSONArray(results.size());
+
+        for (PhotonResult result : results) {
+            final double[] coordinates = result.getCoordinates();
+
+            features.put(new JSONObject()
+                        .put("type", "feature")
+                        .put("properties", getResultProperties(result))
+                        .put("geometry", new JSONObject()
+                                .put("type", "feature")
+                                .put("coordinates", coordinates)));
+        }
+
+        final JSONObject out = new JSONObject();
+        out.put("type", "FeatureCollection")
+           .put("features", features);
+
+        if (debugInfo != null) {
+            out.put("properties", new JSONObject().put("debug", new JSONObject(debugInfo)));
+        }
+
+        if (addDebugInfo) {
+            return out.toString(4);
+        }
+
+        return out.toString();
+    }
+
+    private JSONObject getResultProperties(PhotonResult result) {
+        JSONObject props = new JSONObject();
+        if (addDebugInfo) {
+            props.put("score", result.getScore());
+            put(props,"importance", result.get("importance"));
+        }
+
+        for (String key : KEYS_LANG_UNSPEC) {
+            put(props, key, result.get(key));
+        }
+
+        for (String key : KEYS_LANG_SPEC) {
+            put(props, key, result.getLocalised(key, language));
+        }
+
+        final double[] extent = result.getExtent();
+        if (extent != null) {
+            props.put("extent", extent);
+        }
+
+        put(props, "extra", result.getMap("extra"));
+
+        return props;
+    }
+
+    private void put(JSONObject out, String key, Object value) {
+        if (value != null) {
+            out.put(key, value);
+        }
+    }
+}
diff --git a/src/main/java/de/komoot/photon/searcher/PhotonResult.java b/src/main/java/de/komoot/photon/searcher/PhotonResult.java
new file mode 100644
index 0000000..a68588e
--- /dev/null
+++ b/src/main/java/de/komoot/photon/searcher/PhotonResult.java
@@ -0,0 +1,28 @@
+package de.komoot.photon.searcher;
+
+import java.util.Map;
+
+/**
+ * A single response object from the database.
+ */
+public interface PhotonResult {
+
+    /**
+     * Get the value for the given field.
+     *
+     * Should throw an exception, when the field has multiple values.
+     *
+     * @param key
+     * @return If the field exist, the string value of the field, else null.
+     */
+    Object get(String key);
+
+    String getLocalised(String key, String language);
+    Map<String, String> getMap(String key);
+
+    double[] getCoordinates();
+    double[] getExtent();
+
+    double getScore();
+
+}
diff --git a/src/main/java/de/komoot/photon/searcher/ResultFormatter.java b/src/main/java/de/komoot/photon/searcher/ResultFormatter.java
new file mode 100644
index 0000000..41ec5a8
--- /dev/null
+++ b/src/main/java/de/komoot/photon/searcher/ResultFormatter.java
@@ -0,0 +1,11 @@
+package de.komoot.photon.searcher;
+
+import java.util.List;
+
+/**
+ * Convert a list of results into an output string.
+ */
+public interface ResultFormatter {
+
+    String convert(List<PhotonResult> results, String debugInfo);
+}
diff --git a/src/main/java/de/komoot/photon/searcher/ReverseHandler.java b/src/main/java/de/komoot/photon/searcher/ReverseHandler.java
index 3f2e9d6..0c8aad0 100644
--- a/src/main/java/de/komoot/photon/searcher/ReverseHandler.java
+++ b/src/main/java/de/komoot/photon/searcher/ReverseHandler.java
@@ -7,5 +7,5 @@ import java.util.List;
 
 public interface ReverseHandler {
 
-    List<JSONObject> reverse(ReverseRequest photonRequest);
+    List<PhotonResult> reverse(ReverseRequest photonRequest);
 }
diff --git a/src/main/java/de/komoot/photon/searcher/SearchHandler.java b/src/main/java/de/komoot/photon/searcher/SearchHandler.java
index a79e0d2..70660bc 100644
--- a/src/main/java/de/komoot/photon/searcher/SearchHandler.java
+++ b/src/main/java/de/komoot/photon/searcher/SearchHandler.java
@@ -7,7 +7,7 @@ import java.util.List;
 
 public interface SearchHandler {
 
-    List<JSONObject> search(PhotonRequest photonRequest);
+    List<PhotonResult> search(PhotonRequest photonRequest);
 
     String dumpQuery(PhotonRequest photonRequest);
 }
diff --git a/src/main/java/de/komoot/photon/searcher/StreetDupesRemover.java b/src/main/java/de/komoot/photon/searcher/StreetDupesRemover.java
index d25fa6f..96224ff 100644
--- a/src/main/java/de/komoot/photon/searcher/StreetDupesRemover.java
+++ b/src/main/java/de/komoot/photon/searcher/StreetDupesRemover.java
@@ -1,20 +1,14 @@
 package de.komoot.photon.searcher;
 
-import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
 import de.komoot.photon.Constants;
 import de.komoot.photon.Utils;
-import org.json.JSONObject;
 
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 
 /**
- * This is copy over from the method
- * <pre>private List<JSONObject> removeStreetDuplicates(List<JSONObject> results, String lang)</pre>
- * in class {@link de.komoot.photon.App}
- * <p/>
- * Created by Sachin Dole on 2/20/2015.
+ * Filter out duplicate streets from the list.
  */
 public class StreetDupesRemover {
     private final String language;
@@ -23,26 +17,30 @@ public class StreetDupesRemover {
         this.language = language;
     }
 
-    public List<JSONObject> execute(List<JSONObject>... allResults) {
-        List<JSONObject> results = allResults[0];
-        List<JSONObject> filteredItems = Lists.newArrayListWithCapacity(results.size());
-        final HashSet<String> keys = Sets.newHashSet();
-        for (JSONObject result : results) {
-            final JSONObject properties = result.getJSONObject(Constants.PROPERTIES);
-            if (properties.has(Constants.OSM_KEY) && "highway".equals(properties.getString(Constants.OSM_KEY))) {
+    public List<PhotonResult> execute(List<PhotonResult> results) {
+        final List<PhotonResult> filteredItems = new ArrayList<>(results.size());
+        final HashSet<String> keys = new HashSet<>();
+
+        for (PhotonResult result : results) {
+            if ("highway".equals(result.get(Constants.OSM_KEY))) {
                 // result is a street
-                if (properties.has(Constants.POSTCODE) && properties.has(Constants.NAME)) {
+                final String postcode = (String) result.get(Constants.POSTCODE);
+                final String name = result.getLocalised(Constants.NAME, language);
+
+                if (postcode != null && name != null) {
                     // street has a postcode and name
-                    String postcode = properties.getString(Constants.POSTCODE);
-                    String name = properties.getString(Constants.NAME);
-                    // OSM_VALUE is part of key to avoid deduplication of e.g. bus_stops and streets with same name 
-                    String key = (properties.has(Constants.OSM_VALUE) ? properties.getString(Constants.OSM_VALUE) : "") + ":";
+
+                    // OSM_VALUE is part of key to avoid deduplication of e.g. bus_stops and streets with same name
+                    String key = (String) result.get(Constants.OSM_VALUE);
+                    if (key == null) {
+                        key = "";
+                    }
 
                     if (language.equals("nl")) {
-                        String onlyDigitsPostcode = Utils.stripNonDigits(postcode);
-                        key += onlyDigitsPostcode + ":" + name;
+                        final String onlyDigitsPostcode = Utils.stripNonDigits(postcode);
+                        key += ":" + onlyDigitsPostcode + ":" + name;
                     } else {
-                        key += postcode + ":" + name;
+                        key += ":" + postcode + ":" + name;
                     }
 
                     if (keys.contains(key)) {
@@ -54,6 +52,7 @@ public class StreetDupesRemover {
             }
             filteredItems.add(result);
         }
+
         return filteredItems;
     }
 }
diff --git a/src/main/java/de/komoot/photon/utils/ConvertToGeoJson.java b/src/main/java/de/komoot/photon/utils/ConvertToGeoJson.java
deleted file mode 100644
index 8a67d3c..0000000
--- a/src/main/java/de/komoot/photon/utils/ConvertToGeoJson.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package de.komoot.photon.utils;
-
-import com.google.common.base.Converter;
-import org.json.JSONArray;
-import org.json.JSONObject;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Convert between place list and geojson Created by Sachin Dole on 2/20/2015.
- */
-public class ConvertToGeoJson extends Converter<List<JSONObject>, JSONObject> {
-
-    /**
-     * converts a list of places to a geojson places. Does not validate the list of places. In other words, if json list does not contain valid places, then resulting geojson *
-     * will not be valid.
-     *
-     * @param listOfPlaces places to be converted to geojson
-     * @return geojson object
-     */
-    @Override
-    public JSONObject doForward(List<JSONObject> listOfPlaces) {
-        final JSONObject collection = new JSONObject();
-        collection.put("type", "FeatureCollection");
-        collection.put("features", new JSONArray(listOfPlaces));
-
-        return collection;
-    }
-
-    /**
-     * Extracts the place information from a geojson object and returns a list of those places. Does not validate that the geojson object is valid or that the contents are * indeed
-     * places.
-     *
-     * @param jsonObject a list of places in geojson format
-     * @return the list of places from the geojson.
-     */
-    @Override
-    public List<JSONObject> doBackward(JSONObject jsonObject) {
-        JSONArray features = jsonObject.getJSONArray("features");
-        List<JSONObject> returnValue = new ArrayList<JSONObject>(features.length());
-        for (int i = 0; i < features.length(); i++) {
-            JSONObject aSearchHit = features.getJSONObject(i);
-            returnValue.add(aSearchHit);
-        }
-        return returnValue;
-    }
-}
diff --git a/src/main/java/de/komoot/photon/utils/ConvertToJson.java b/src/main/java/de/komoot/photon/utils/ConvertToJson.java
deleted file mode 100644
index 673ebac..0000000
--- a/src/main/java/de/komoot/photon/utils/ConvertToJson.java
+++ /dev/null
@@ -1,115 +0,0 @@
-package de.komoot.photon.utils;
-
-import com.google.common.collect.Lists;
-import de.komoot.photon.Constants;
-import lombok.extern.slf4j.Slf4j;
-import org.elasticsearch.action.search.SearchResponse;
-import org.elasticsearch.search.SearchHit;
-import org.json.JSONArray;
-import org.json.JSONObject;
-
-import java.util.List;
-import java.util.Map;
-
-/**
- * Convert a elasticsearch {@link SearchResponse} into a list of {@link JSONObject}s
- * <p/>
- * Created by Sachin Dole on 2/20/2015.
- */
-@Slf4j
-public class ConvertToJson {
-    private static final String[] KEYS_LANG_UNSPEC = {Constants.OSM_ID, Constants.OSM_VALUE, Constants.OSM_KEY, Constants.POSTCODE, Constants.HOUSENUMBER, Constants.COUNTRYCODE, Constants.OSM_TYPE};
-    private static final String[] KEYS_LANG_SPEC = {Constants.NAME, Constants.COUNTRY, Constants.CITY, Constants.DISTRICT, Constants.LOCALITY, Constants.STREET, Constants.STATE, Constants.COUNTY};
-    private static final String[] NAME_PRECEDENCE = {"default", "housename", "int", "loc", "reg", "alt", "old"};
-    private final String lang;
-
-    public ConvertToJson(String lang) {
-        this.lang = lang;
-    }
-
-    public List<JSONObject> convert(SearchResponse searchResponse, boolean debugMode) {
-        SearchHit[] hits = searchResponse.getHits().getHits();
-        final List<JSONObject> list = Lists.newArrayListWithExpectedSize(hits.length);
-        for (SearchHit hit : hits) {
-            final Map<String, Object> source = hit.getSource();
-
-            final JSONObject feature = new JSONObject();
-            if (debugMode) {
-                feature.put("score", hit.getScore());
-                feature.put("importance", source.get("importance"));
-            }
-            feature.put(Constants.TYPE, Constants.FEATURE);
-            feature.put(Constants.GEOMETRY, getPoint(source));
-
-            // populate properties
-            final JSONObject properties = new JSONObject();
-
-            // language unspecific properties
-            for (String key : KEYS_LANG_UNSPEC) {
-                if (source.containsKey(key))
-                    properties.put(key, source.get(key));
-            }
-
-            // language specific properties
-            for (String key : KEYS_LANG_SPEC) {
-                if (source.containsKey(key))
-                    properties.put(key, getLocalised(source, key, lang));
-            }
-
-            // place type
-            properties.put("type", source.get(Constants.OBJECT_TYPE));
-
-            // add extent of geometry
-            final Map<String, Object> extent = (Map<String, Object>) source.get("extent");
-            if (extent != null) {
-                List<List<Double>> coords = (List<List<Double>>) extent.get("coordinates");
-                final List<Double> nw = coords.get(0);
-                final List<Double> se = coords.get(1);
-                properties.put("extent", new JSONArray(Lists.newArrayList(nw.get(0), nw.get(1), se.get(0), se.get(1))));
-            }
-
-            final Map<String, String> extraTags = (Map<String, String>) source.get("extra");
-            if (extraTags != null) {
-                properties.put("extra", extraTags);
-            }
-
-            feature.put(Constants.PROPERTIES, properties);
-
-            list.add(feature);
-        }
-        return list;
-    }
-
-    private String getLocalised(Map<String, Object> source, String fieldName, String lang) {
-        final Map<String, String> map = (Map<String, String>) source.get(fieldName);
-        if (map == null) return null;
-
-        if (map.get(lang) != null) {
-            // language specific field
-            return map.get(lang);
-        }
-
-        if (fieldName.equals("name")) {
-            for (String key : NAME_PRECEDENCE) {
-                if (map.containsKey(key))
-                    return map.get(key);
-            }
-        }
-
-        return map.get("default");
-    }
-
-    private JSONObject getPoint(Map<String, Object> source) {
-        JSONObject point = new JSONObject();
-
-        final Map<String, Double> coordinate = (Map<String, Double>) source.get("coordinate");
-        if (coordinate != null) {
-            point.put(Constants.TYPE, Constants.POINT);
-            point.put(Constants.COORDINATES, new JSONArray("[" + coordinate.get(Constants.LON) + "," + coordinate.get(Constants.LAT) + "]"));
-        } else {
-            log.error(String.format("invalid data [id=%s, type=%s], coordinate is missing!", source.get(Constants.OSM_ID), source.get(Constants.OSM_VALUE)));
-        }
-
-        return point;
-    }
-}


Test patch:
diff --git a/src/test/java/de/komoot/photon/query/QueryBasicSearchTest.java b/src/test/java/de/komoot/photon/query/QueryBasicSearchTest.java
index e2cfda6..fcb140e 100644
--- a/src/test/java/de/komoot/photon/query/QueryBasicSearchTest.java
+++ b/src/test/java/de/komoot/photon/query/QueryBasicSearchTest.java
@@ -3,7 +3,7 @@ package de.komoot.photon.query;
 import de.komoot.photon.ESBaseTester;
 import de.komoot.photon.Importer;
 import de.komoot.photon.PhotonDoc;
-import org.json.JSONObject;
+import de.komoot.photon.searcher.PhotonResult;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
@@ -38,7 +38,7 @@ public class QueryBasicSearchTest extends ESBaseTester {
         return new PhotonDoc(testDocId, "N", testDocId, "place", "city").names(nameMap);
     }
 
-    private List<JSONObject> search(String query) {
+    private List<PhotonResult> search(String query) {
         return getServer().createSearchHandler(new String[]{"en"}).search(new PhotonRequest(query, 10, null, null, 0.2, 14, "en", false));
     }
 
diff --git a/src/test/java/de/komoot/photon/query/QueryByClassificationTest.java b/src/test/java/de/komoot/photon/query/QueryByClassificationTest.java
index 8e344f7..728e940 100644
--- a/src/test/java/de/komoot/photon/query/QueryByClassificationTest.java
+++ b/src/test/java/de/komoot/photon/query/QueryByClassificationTest.java
@@ -1,6 +1,7 @@
 package de.komoot.photon.query;
 
 import de.komoot.photon.*;
+import de.komoot.photon.searcher.PhotonResult;
 import lombok.extern.slf4j.Slf4j;
 import org.elasticsearch.action.get.GetResponse;
 import org.json.JSONArray;
@@ -34,7 +35,7 @@ public class QueryByClassificationTest extends ESBaseTester {
         return new PhotonDoc(testDocId, "W", testDocId, key, value).names(Collections.singletonMap("name", name));
     }
 
-    private List<JSONObject> search(String query) {
+    private List<PhotonResult> search(String query) {
         return getServer().createSearchHandler(new String[]{"en"}).search(new PhotonRequest(query));
     }
 
@@ -85,10 +86,10 @@ public class QueryByClassificationTest extends ESBaseTester {
         String classification = (String) response.getSource().get(Constants.CLASSIFICATION);
         assertEquals(classification, class_term);
 
-        List<JSONObject> result = search(class_term + " curli");
+        List<PhotonResult> result = search(class_term + " curli");
 
         assertTrue(result.size() > 0);
-        assertEquals(testDocId, result.get(0).getJSONObject("properties").getInt("osm_id"));
+        assertEquals(testDocId, result.get(0).get("osm_id"));
     }
 
     @Test
@@ -100,14 +101,14 @@ public class QueryByClassificationTest extends ESBaseTester {
 
         updateClassification("amenity", "restaurant", "pub", "kneipe");
 
-        List<JSONObject> result = search("pub curli");
+        List<PhotonResult> result = search("pub curli");
         assertTrue(result.size() > 0);
-        assertEquals(testDocId, result.get(0).getJSONObject("properties").getInt("osm_id"));
+        assertEquals(testDocId, result.get(0).get("osm_id"));
 
 
         result = search("curliflower kneipe");
         assertTrue(result.size() > 0);
-        assertEquals(testDocId, result.get(0).getJSONObject("properties").getInt("osm_id"));
+        assertEquals(testDocId, result.get(0).get("osm_id"));
     }
 
 
@@ -121,14 +122,14 @@ public class QueryByClassificationTest extends ESBaseTester {
 
         updateClassification("aeroway", "terminal", "airport");
 
-        List<JSONObject> result = search("airport");
+        List<PhotonResult> result = search("airport");
         assertTrue(result.size() > 0);
-        assertEquals(testDocId - 1, result.get(0).getJSONObject("properties").getInt("osm_id"));
+        assertEquals(testDocId - 1, result.get(0).get("osm_id"));
 
 
         result = search("airport houston");
         assertTrue(result.size() > 0);
-        assertEquals(testDocId, result.get(0).getJSONObject("properties").getInt("osm_id"));
+        assertEquals(testDocId, result.get(0).get("osm_id"));
     }
 
     @Test
@@ -167,16 +168,16 @@ public class QueryByClassificationTest extends ESBaseTester {
 
         getServer().waitForReady();
 
-        List<JSONObject> result = search("Station newtown");
+        List<PhotonResult> result = search("Station newtown");
         assertTrue(result.size() > 0);
-        assertEquals(testDocId - 1, result.get(0).getJSONObject("properties").getInt("osm_id"));
+        assertEquals(testDocId - 1, result.get(0).get("osm_id"));
 
         result = search("newtown stop");
         assertTrue(result.size() > 0);
-        assertEquals(testDocId - 1, result.get(0).getJSONObject("properties").getInt("osm_id"));
+        assertEquals(testDocId - 1, result.get(0).get("osm_id"));
 
         result = search("king's cross Station");
         assertTrue(result.size() > 0);
-        assertEquals(testDocId, result.get(0).getJSONObject("properties").getInt("osm_id"));
+        assertEquals(testDocId, result.get(0).get("osm_id"));
     }
 }
diff --git a/src/test/java/de/komoot/photon/query/QueryByLanguageTest.java b/src/test/java/de/komoot/photon/query/QueryByLanguageTest.java
index ab04e97..e703371 100644
--- a/src/test/java/de/komoot/photon/query/QueryByLanguageTest.java
+++ b/src/test/java/de/komoot/photon/query/QueryByLanguageTest.java
@@ -4,8 +4,8 @@ import de.komoot.photon.ESBaseTester;
 import de.komoot.photon.PhotonDoc;
 import de.komoot.photon.Importer;
 import de.komoot.photon.nominatim.model.AddressType;
+import de.komoot.photon.searcher.PhotonResult;
 import lombok.extern.slf4j.Slf4j;
-import org.json.JSONObject;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.EnumSource;
@@ -41,7 +41,7 @@ public class QueryByLanguageTest extends ESBaseTester {
         return new PhotonDoc(testDocId, "W", testDocId, "place", "city").names(nameMap);
     }
 
-    private List<JSONObject> search(String query, String lang) {
+    private List<PhotonResult> search(String query, String lang) {
         return getServer().createSearchHandler(languageList).search(new PhotonRequest(query, 10, null, null, 0.2, 14, lang, true));
     }
 
@@ -58,8 +58,8 @@ public class QueryByLanguageTest extends ESBaseTester {
         assertEquals(1, search("finish", "en").size());
         assertEquals(0, search("russian", "en").size());
 
-        float enScore = search("finish", "en").get(0).getFloat("score");
-        float fiScore = search("finish", "fi").get(0).getFloat("score");
+        double enScore = search("finish", "en").get(0).getScore();
+        double fiScore = search("finish", "fi").get(0).getScore();
 
         assertTrue(fiScore > enScore);
     }
diff --git a/src/test/java/de/komoot/photon/query/QueryFilterTagValueTest.java b/src/test/java/de/komoot/photon/query/QueryFilterTagValueTest.java
index 7a8605c..29b8981 100644
--- a/src/test/java/de/komoot/photon/query/QueryFilterTagValueTest.java
+++ b/src/test/java/de/komoot/photon/query/QueryFilterTagValueTest.java
@@ -6,6 +6,7 @@ import de.komoot.photon.Importer;
 
 import java.util.List;
 
+import de.komoot.photon.searcher.PhotonResult;
 import org.json.JSONObject;
 import org.junit.jupiter.api.*;
 
@@ -53,7 +54,7 @@ public class QueryFilterTagValueTest extends ESBaseTester {
         return request;
     }
 
-    private List<JSONObject> search(PhotonRequest request) {
+    private List<PhotonResult> search(PhotonRequest request) {
         return getServer().createSearchHandler(new String[]{"en"}).search(request);
     }
 
@@ -62,7 +63,7 @@ public class QueryFilterTagValueTest extends ESBaseTester {
      */
     @Test
     public void testFilterWithTagTourismAttraction() {
-        List<JSONObject> searchResponse = search(queryBerlinWithTags("tourism:attraction"));
+        List<PhotonResult> searchResponse = search(queryBerlinWithTags("tourism:attraction"));
 
         assertEquals(2l, searchResponse.size());
     }
@@ -72,7 +73,7 @@ public class QueryFilterTagValueTest extends ESBaseTester {
      */
     @Test
     public void testValueAttraction() {
-        List<JSONObject> searchResponse = search(queryBerlinWithTags(":attraction"));
+        List<PhotonResult> searchResponse = search(queryBerlinWithTags(":attraction"));
 
         assertEquals(2l, searchResponse.size());
     }
@@ -82,7 +83,7 @@ public class QueryFilterTagValueTest extends ESBaseTester {
      */
     @Test
     public void testKeyTourism() {
-        List<JSONObject> searchResponse = search(queryBerlinWithTags("tourism"));
+        List<PhotonResult> searchResponse = search(queryBerlinWithTags("tourism"));
 
         assertEquals(8l, searchResponse.size());
     }
@@ -92,7 +93,7 @@ public class QueryFilterTagValueTest extends ESBaseTester {
      */
     @Test
     public void testFilterWithoutTagTourismAttraction() {
-        List<JSONObject> searchResponse = search(queryBerlinWithTags("!tourism:attraction"));
+        List<PhotonResult> searchResponse = search(queryBerlinWithTags("!tourism:attraction"));
 
         assertEquals(16l, searchResponse.size());
     }
@@ -102,7 +103,7 @@ public class QueryFilterTagValueTest extends ESBaseTester {
      */
     @Test
     public void testValueNotInformation() {
-        List<JSONObject> searchResponse = search(queryBerlinWithTags("!:information"));
+        List<PhotonResult> searchResponse = search(queryBerlinWithTags("!:information"));
 
         assertEquals(12l, searchResponse.size());
     }
@@ -112,7 +113,7 @@ public class QueryFilterTagValueTest extends ESBaseTester {
      */
     @Test
     public void testValueNotInformationAlt() {
-        List<JSONObject> searchResponse = search(queryBerlinWithTags(":!information"));
+        List<PhotonResult> searchResponse = search(queryBerlinWithTags(":!information"));
 
         assertEquals(12l, searchResponse.size());
     }
@@ -122,7 +123,7 @@ public class QueryFilterTagValueTest extends ESBaseTester {
      */
     @Test
     public void testKeyNotTourism() {
-        List<JSONObject> searchResponse = search(queryBerlinWithTags("!tourism"));
+        List<PhotonResult> searchResponse = search(queryBerlinWithTags("!tourism"));
 
         assertEquals(10l, searchResponse.size());
     }
@@ -132,7 +133,7 @@ public class QueryFilterTagValueTest extends ESBaseTester {
      */
     @Test
     public void testKeyTourismAndValueNotInformation() {
-        List<JSONObject> searchResponse = search(queryBerlinWithTags("tourism:!information"));
+        List<PhotonResult> searchResponse = search(queryBerlinWithTags("tourism:!information"));
 
         assertEquals(6l, searchResponse.size());
     }
@@ -142,7 +143,7 @@ public class QueryFilterTagValueTest extends ESBaseTester {
      */
     @Test
     public void testKeyNotTourismAndKeyNotAmenity() {
-        List<JSONObject> searchResponse = search(queryBerlinWithTags("!tourism", "!amenity"));
+        List<PhotonResult> searchResponse = search(queryBerlinWithTags("!tourism", "!amenity"));
 
         assertEquals(4l, searchResponse.size());
     }
@@ -154,7 +155,7 @@ public class QueryFilterTagValueTest extends ESBaseTester {
      */
     @Test
     public void testKeyTourismAndKeyNotAmenity() {
-        List<JSONObject> searchResponse = search(queryBerlinWithTags("tourism", "!amenity"));
+        List<PhotonResult> searchResponse = search(queryBerlinWithTags("tourism", "!amenity"));
 
         assertEquals(8l, searchResponse.size());
     }
@@ -164,7 +165,7 @@ public class QueryFilterTagValueTest extends ESBaseTester {
      */
     @Test
     public void testValueInformationButKeyNotAmenity() {
-        List<JSONObject> searchResponse = search(queryBerlinWithTags(":information", "!amenity"));
+        List<PhotonResult> searchResponse = search(queryBerlinWithTags(":information", "!amenity"));
 
         assertEquals(4l, searchResponse.size());
     }
@@ -174,7 +175,7 @@ public class QueryFilterTagValueTest extends ESBaseTester {
      */
     @Test
     public void testTagNotTourismAttraction() {
-        List<JSONObject> searchResponse = search(queryBerlinWithTags("!tourism:attraction"));
+        List<PhotonResult> searchResponse = search(queryBerlinWithTags("!tourism:attraction"));
 
         assertEquals(16l, searchResponse.size());
     }
diff --git a/src/test/java/de/komoot/photon/searcher/MockPhotonResult.java b/src/test/java/de/komoot/photon/searcher/MockPhotonResult.java
new file mode 100644
index 0000000..03ba9da
--- /dev/null
+++ b/src/test/java/de/komoot/photon/searcher/MockPhotonResult.java
@@ -0,0 +1,52 @@
+package de.komoot.photon.searcher;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class MockPhotonResult implements PhotonResult {
+
+    final Map<String, Object> data = new HashMap<>();
+    final double[] coordinates = new double[]{42, 21};
+    final double[] extent = new double[]{0, 1, 2, 3};
+    final Map<String, String> localized = new HashMap<>();
+
+    @Override
+    public Object get(String key) {
+        return data.getOrDefault(key, null);
+    }
+
+    @Override
+    public String getLocalised(String key, String language) {
+        return localized.getOrDefault(key + "||" + language, null);
+    }
+
+    @Override
+    public Map<String, String> getMap(String key) {
+        return (Map<String, String>) data.getOrDefault(key, null);
+    }
+
+    @Override
+    public double[] getCoordinates() {
+        return coordinates;
+    }
+
+    @Override
+    public double[] getExtent() {
+        return extent;
+    }
+
+    @Override
+    public double getScore() {
+        return 99;
+    }
+
+    public MockPhotonResult put(String key, Object value) {
+        data.put(key, value);
+        return this;
+    }
+
+    public MockPhotonResult putLocalized(String key, String lang, String value) {
+        localized.put(key + "||" + lang, value);
+        return this;
+    }
+}
diff --git a/src/test/java/de/komoot/photon/searcher/StreetDupesRemoverTest.java b/src/test/java/de/komoot/photon/searcher/StreetDupesRemoverTest.java
index f4e6118..c5bcf22 100644
--- a/src/test/java/de/komoot/photon/searcher/StreetDupesRemoverTest.java
+++ b/src/test/java/de/komoot/photon/searcher/StreetDupesRemoverTest.java
@@ -14,30 +14,32 @@ public class StreetDupesRemoverTest {
     @Test
     public void testDeduplicatesStreets() {
         StreetDupesRemover streetDupesRemover = new StreetDupesRemover("en");
-        List<JSONObject> allResults = new ArrayList<>();
+        List<PhotonResult> allResults = new ArrayList<>();
         allResults.add(createDummyResult("99999", "Main Street", "highway", "Unclassified"));
         allResults.add(createDummyResult("99999", "Main Street", "highway", "Unclassified"));
 
-        List<JSONObject> dedupedResults = streetDupesRemover.execute(allResults);
+        List<PhotonResult> dedupedResults = streetDupesRemover.execute(allResults);
         assertEquals(1, dedupedResults.size());
     }
 
     @Test
     public void testStreetAndBusStopNotDeduplicated() {
         StreetDupesRemover streetDupesRemover = new StreetDupesRemover("en");
-        List<JSONObject> allResults = new ArrayList<>();
+        List<PhotonResult> allResults = new ArrayList<>();
         allResults.add(createDummyResult("99999", "Main Street", "highway", "bus_stop"));
         allResults.add(createDummyResult("99999", "Main Street", "highway", "Unclassified"));
 
-        List<JSONObject> dedupedResults = streetDupesRemover.execute(allResults);
+        List<PhotonResult> dedupedResults = streetDupesRemover.execute(allResults);
         assertEquals(2, dedupedResults.size());
     }
     
-    private JSONObject createDummyResult(String postCode, String name, String osmKey,
+    private PhotonResult createDummyResult(String postCode, String name, String osmKey,
                     String osmValue) {
-        return new JSONObject().put(Constants.PROPERTIES, new JSONObject()
-                        .put(Constants.POSTCODE, postCode).put(Constants.NAME, name)
-                        .put(Constants.OSM_KEY, osmKey).put(Constants.OSM_VALUE, osmValue));
+        return new MockPhotonResult()
+                .put(Constants.POSTCODE, postCode)
+                .putLocalized(Constants.NAME, "en", name)
+                .put(Constants.OSM_KEY, osmKey)
+                .put(Constants.OSM_VALUE, osmValue);
     }
 
 }
diff --git a/src/test/java/de/komoot/photon/utils/ConvertToJsonTest.java b/src/test/java/de/komoot/photon/utils/ConvertToJsonTest.java
deleted file mode 100644
index 436b7fd..0000000
--- a/src/test/java/de/komoot/photon/utils/ConvertToJsonTest.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package de.komoot.photon.utils;
-
-import de.komoot.photon.ESBaseTester;
-import de.komoot.photon.PhotonDoc;
-import de.komoot.photon.Importer;
-import de.komoot.photon.query.PhotonRequest;
-import org.json.JSONObject;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-import java.io.IOException;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class ConvertToJsonTest extends ESBaseTester {
-
-    private List<JSONObject> databaseFromDoc(PhotonDoc doc) throws IOException {
-        final String name = "abcde";
-        doc.names(Collections.singletonMap("name", name));
-
-        setUpES();
-        Importer instance = makeImporterWithExtra("maxspeed","website");
-        instance.add(doc);
-        instance.finish();
-        refresh();
-
-        // Indirectly calls converttojson
-        return getServer().createSearchHandler(new String[]{"en", "de", "fr"}).search(new PhotonRequest(name));
-    }
-
-    @Test
-    public void testConvertWithExtraTags() throws IOException {
-        Map<String, String> extratags = new HashMap<>();
-        extratags.put("website", "foo");
-        extratags.put("maxspeed", "100 mph");
-
-        List<JSONObject> json = databaseFromDoc(new PhotonDoc(1234, "N", 1000, "place", "city").extraTags(extratags));
-
-        JSONObject extra = json.get(0).getJSONObject("properties").getJSONObject("extra");
-
-        assertEquals(2, extra.length());
-        assertEquals("foo", extra.getString("website"));
-        assertEquals("100 mph", extra.getString("maxspeed"));
-    }
-
-
-    @Test
-    public void testConvertWithoutExtraTags() throws IOException {
-        List<JSONObject> json = databaseFromDoc(new PhotonDoc(1234, "N", 1000, "place", "city"));
-
-        assertNull(json.get(0).getJSONObject("properties").optJSONObject("extra"));
-    }
-}