]> git.openstreetmap.org Git - nominatim.git/commitdiff
Merge remote-tracking branch 'upstream/master'
authorSarah Hoffmann <lonvia@denofr.de>
Tue, 10 Oct 2017 19:25:43 +0000 (21:25 +0200)
committerSarah Hoffmann <lonvia@denofr.de>
Tue, 10 Oct 2017 19:25:43 +0000 (21:25 +0200)
15 files changed:
lib/Geocode.php
lib/NearPoint.php [deleted file]
lib/ReverseGeocode.php
lib/SearchContext.php [new file with mode: 0644]
lib/SearchDescription.php [new file with mode: 0644]
lib/SpecialSearchOperator.php [new file with mode: 0644]
lib/db.php
lib/lib.php
phpcs.xml
settings/defaults.php
test/php/Nominatim/NearPointTest.php [deleted file]
test/php/Nominatim/NominatimTest.php
test/php/Nominatim/SearchContextTest.php [new file with mode: 0644]
utils/setup.php
utils/update.php

index 15915f940133273991916093a6d145e75cbe8ece..4b2ae66226c44a67d731949e166d9f9c48e23fdc 100644 (file)
@@ -2,9 +2,10 @@
 
 namespace Nominatim;
 
-require_once(CONST_BasePath.'/lib/NearPoint.php');
 require_once(CONST_BasePath.'/lib/PlaceLookup.php');
 require_once(CONST_BasePath.'/lib/ReverseGeocode.php');
+require_once(CONST_BasePath.'/lib/SearchDescription.php');
+require_once(CONST_BasePath.'/lib/SearchContext.php');
 
 class Geocode
 {
@@ -36,9 +37,8 @@ class Geocode
 
     protected $bBoundedSearch = false;
     protected $aViewBox = false;
-    protected $sViewboxCentreSQL = false;
-    protected $sViewboxSmallSQL = false;
-    protected $sViewboxLargeSQL = false;
+    protected $aRoutePoints = false;
+    protected $aRouteWidth = false;
 
     protected $iMaxRank = 20;
     protected $iMinAddressRank = 0;
@@ -183,26 +183,6 @@ class Geocode
         $this->iMaxAddressRank = $iMax;
     }
 
-    public function setRoute($aRoutePoints, $fRouteWidth)
-    {
-        $this->aViewBox = false;
-
-        $this->sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
-        $sSep = '';
-        foreach ($aRoutePoints as $aPoint) {
-            $fPoint = (float)$aPoint;
-            $this->sViewboxCentreSQL .= $sSep.$fPoint;
-            $sSep = ($sSep == ' ') ? ',' : ' ';
-        }
-        $this->sViewboxCentreSQL .= ")'::geometry,4326)";
-
-        $this->sViewboxSmallSQL = 'ST_BUFFER('.$this->sViewboxCentreSQL;
-        $this->sViewboxSmallSQL .= ','.($fRouteWidth/69).')';
-
-        $this->sViewboxLargeSQL = 'ST_BUFFER('.$this->sViewboxCentreSQL;
-        $this->sViewboxLargeSQL .= ','.($fRouteWidth/30).')';
-    }
-
     public function setViewbox($aViewbox)
     {
         $this->aViewBox = array_map('floatval', $aViewbox);
@@ -217,29 +197,6 @@ class Geocode
         ) {
             userError("Bad parameter 'viewbox'. Not a box.");
         }
-
-        $fHeight = $this->aViewBox[0] - $this->aViewBox[2];
-        $fWidth = $this->aViewBox[1] - $this->aViewBox[3];
-        $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
-        $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
-        $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
-        $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
-
-        $this->sViewboxCentreSQL = false;
-        $this->sViewboxSmallSQL = sprintf(
-            'ST_SetSRID(ST_MakeBox2D(ST_Point(%F,%F),ST_Point(%F,%F)),4326)',
-            $this->aViewBox[0],
-            $this->aViewBox[1],
-            $this->aViewBox[2],
-            $this->aViewBox[3]
-        );
-        $this->sViewboxLargeSQL = sprintf(
-            'ST_SetSRID(ST_MakeBox2D(ST_Point(%F,%F),ST_Point(%F,%F)),4326)',
-            $aBigViewBox[0],
-            $aBigViewBox[1],
-            $aBigViewBox[2],
-            $aBigViewBox[3]
-        );
     }
 
     public function setQuery($sQueryString)
@@ -318,7 +275,8 @@ class Geocode
                 $aRoute = $oParams->getStringList('route');
                 $fRouteWidth = $oParams->getFloat('routewidth');
                 if ($aRoute && $fRouteWidth) {
-                    $this->setRoute($aRoute, $fRouteWidth);
+                    $this->aRoutePoints = $aRoute;
+                    $this->aRouteWidth = $fRouteWidth;
                 }
             }
         }
@@ -367,7 +325,7 @@ class Geocode
         $this->aAddressRankList = array();
 
         $this->aStructuredQuery = array();
-        $this->sAllowedTypesSQLList = False;
+        $this->sAllowedTypesSQLList = false;
 
         $this->loadStructuredAddressElement($sAmenity, 'amenity', 26, 30, false);
         $this->loadStructuredAddressElement($sStreet, 'street', 26, 30, false);
@@ -406,26 +364,20 @@ class Geocode
         return false;
     }
 
-    public function getDetails($aPlaceIDs)
+    public function getDetails($aPlaceIDs, $oCtx)
     {
         //$aPlaceIDs is an array with key: placeID and value: tiger-housenumber, if found, else -1
         if (sizeof($aPlaceIDs) == 0) return array();
 
-        $sLanguagePrefArraySQL = "ARRAY[".join(',', array_map("getDBQuoted", $this->aLangPrefOrder))."]";
+        $sLanguagePrefArraySQL = getArraySQL(
+            array_map("getDBQuoted", $this->aLangPrefOrder)
+        );
 
         // Get the details for display (is this a redundant extra step?)
         $sPlaceIDs = join(',', array_keys($aPlaceIDs));
 
-        $sImportanceSQL = '';
-        $sImportanceSQLGeom = '';
-        if ($this->sViewboxSmallSQL) {
-            $sImportanceSQL .= " CASE WHEN ST_Contains($this->sViewboxSmallSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
-            $sImportanceSQLGeom .= " CASE WHEN ST_Contains($this->sViewboxSmallSQL, geometry) THEN 1 ELSE 0.75 END * ";
-        }
-        if ($this->sViewboxLargeSQL) {
-            $sImportanceSQL .= " CASE WHEN ST_Contains($this->sViewboxLargeSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
-            $sImportanceSQLGeom .= " CASE WHEN ST_Contains($this->sViewboxLargeSQL, geometry) THEN 1 ELSE 0.75 END * ";
-        }
+        $sImportanceSQL = $oCtx->viewboxImportanceSQL('ST_Collect(centroid)');
+        $sImportanceSQLGeom = $oCtx->viewboxImportanceSQL('geometry');
 
         $sSQL  = "SELECT ";
         $sSQL .= "    osm_type,";
@@ -445,7 +397,7 @@ class Geocode
         if ($this->bIncludeNameDetails) $sSQL .= "hstore_to_json(name)::text AS names,";
         $sSQL .= "    avg(ST_X(centroid)) AS lon, ";
         $sSQL .= "    avg(ST_Y(centroid)) AS lat, ";
-        $sSQL .= "    ".$sImportanceSQL."COALESCE(importance,0.75-(rank_search::float/40)) AS importance, ";
+        $sSQL .= "    COALESCE(importance,0.75-(rank_search::float/40)) $sImportanceSQL AS importance, ";
         $sSQL .= "    ( ";
         $sSQL .= "       SELECT max(p.importance*(p.rank_address+2))";
         $sSQL .= "       FROM ";
@@ -504,7 +456,7 @@ class Geocode
         if ($this->bIncludeExtraTags) $sSQL .= "null AS extra,";
         if ($this->bIncludeNameDetails) $sSQL .= "null AS names,";
         $sSQL .= "  ST_x(st_centroid(geometry)) AS lon, ST_y(st_centroid(geometry)) AS lat,";
-        $sSQL .=    $sImportanceSQLGeom."(0.75-(rank_search::float/40)) AS importance, ";
+        $sSQL .= "  (0.75-(rank_search::float/40)) $sImportanceSQLGeom AS importance, ";
         $sSQL .= "  (";
         $sSQL .= "     SELECT max(p.importance*(p.rank_address+2))";
         $sSQL .= "     FROM ";
@@ -552,7 +504,7 @@ class Geocode
                 if ($this->bIncludeNameDetails) $sSQL .= "null AS names,";
                 $sSQL .= "     avg(st_x(centroid)) AS lon, ";
                 $sSQL .= "     avg(st_y(centroid)) AS lat,";
-                $sSQL .= "     ".$sImportanceSQL."-1.15 AS importance, ";
+                $sSQL .= "     -1.15".$sImportanceSQL." AS importance, ";
                 $sSQL .= "     (";
                 $sSQL .= "        SELECT max(p.importance*(p.rank_address+2))";
                 $sSQL .= "        FROM ";
@@ -602,7 +554,7 @@ class Geocode
             if ($this->bIncludeNameDetails) $sSQL .= "null AS names, ";
             $sSQL .= "  AVG(st_x(centroid)) AS lon, ";
             $sSQL .= "  AVG(st_y(centroid)) AS lat, ";
-            $sSQL .= "  ".$sImportanceSQL."-0.1 AS importance, ";  // slightly smaller than the importance for normal houses with rank 30, which is 0
+            $sSQL .= "  -0.1".$sImportanceSQL." AS importance, ";  // slightly smaller than the importance for normal houses with rank 30, which is 0
             $sSQL .= "  (";
             $sSQL .= "     SELECT ";
             $sSQL .= "       MAX(p.importance*(p.rank_address+2)) ";
@@ -661,7 +613,7 @@ class Geocode
                 if ($this->bIncludeNameDetails) $sSQL .= "null AS names, ";
                 $sSQL .= "     avg(ST_X(centroid)) AS lon, ";
                 $sSQL .= "     avg(ST_Y(centroid)) AS lat, ";
-                $sSQL .= "     ".$sImportanceSQL."-1.10 AS importance, ";
+                $sSQL .= "     -1.10".$sImportanceSQL." AS importance, ";
                 $sSQL .= "     ( ";
                 $sSQL .= "       SELECT max(p.importance*(p.rank_address+2))";
                 $sSQL .= "       FROM ";
@@ -713,8 +665,11 @@ class Geocode
 
         foreach ($aPhrases as $iPhrase => $aPhrase) {
             $aNewPhraseSearches = array();
-            if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
-            else $sPhraseType = '';
+            if ($bStructuredPhrases) {
+                $sPhraseType = $aPhraseTypes[$iPhrase];
+            } else {
+                $sPhraseType = '';
+            }
 
             foreach ($aPhrase['wordsets'] as $iWordSet => $aWordset) {
                 // Too many permutations - too expensive
@@ -727,169 +682,72 @@ class Geocode
                     //echo "<br><b>$sToken</b>";
                     $aNewWordsetSearches = array();
 
-                    foreach ($aWordsetSearches as $aCurrentSearch) {
+                    foreach ($aWordsetSearches as $oCurrentSearch) {
                         //echo "<i>";
-                        //var_dump($aCurrentSearch);
+                        //var_dump($oCurrentSearch);
                         //echo "</i>";
 
                         // If the token is valid
                         if (isset($aValidTokens[' '.$sToken])) {
-                            // TODO variable should go into aCurrentSearch
-                            $bHavePostcode = false;
                             foreach ($aValidTokens[' '.$sToken] as $aSearchTerm) {
-                                $aSearch = $aCurrentSearch;
-                                $aSearch['iSearchRank']++;
-                                if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0') {
-                                    if ($aSearch['sCountryCode'] === false) {
-                                        $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
-                                        // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
-                                        if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases))) {
-                                            $aSearch['iSearchRank'] += 5;
-                                        }
-                                        if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
-                                        // If it is at the beginning, we can be almost sure that this is the wrong order
-                                        // Increase score for all searches.
-                                        if ($iToken == 0 && $iPhrase == 0) {
-                                            $iGlobalRank++;
-                                        }
-                                    }
-                                } elseif (($sPhraseType == '' || $sPhraseType == 'postalcode') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'postcode') {
-                                    // We need to try the case where the postal code is the primary element (i.e. no way to tell if it is (postalcode, city) OR (city, postalcode) so try both
-                                    if ($aSearch['sPostcode'] === '' &&
-                                        isset($aSearchTerm['word']) && $aSearchTerm['word'] && strpos($sNormQuery, $this->normTerm($aSearchTerm['word'])) !== false) {
-                                        // If we have structured search or this is the first term,
-                                        // make the postcode the primary search element.
-                                        if (!$bHavePostcode && $aSearch['sOperator'] === '' && ($sPhraseType == 'postalcode' || ($iToken == 0 && $iPhrase == 0))) {
-                                            $aNewSearch = $aSearch;
-                                            $aNewSearch['sOperator'] = 'postcode';
-                                            $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
-                                            $aNewSearch['aName'] = array($aSearchTerm['word_id'] => $aSearchTerm['word']);
-                                            if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
-                                            $bHavePostcode = true;
-                                        }
-
-                                        // If we have a structured search or this is not the first term,
-                                        // add the postcode as an addendum.
-                                        if ($aSearch['sOperator'] !== 'postcode' && ($sPhraseType == 'postalcode' || sizeof($aSearch['aName']))) {
-                                            $aSearch['sPostcode'] = $aSearchTerm['word'];
-                                            if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
-                                        }
-                                    }
-                                } elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house') {
-                                    if ($aSearch['sHouseNumber'] === '' && $aSearch['sOperator'] !== 'postcode') {
-                                        $aSearch['sHouseNumber'] = $sToken;
-                                        // sanity check: if the housenumber is not mainly made
-                                        // up of numbers, add a penalty
-                                        if (preg_match_all("/[^0-9]/", $sToken, $aMatches) > 2) $aSearch['iSearchRank']++;
-                                        // also must not appear in the middle of the address
-                                        if ($aSearch['aAddress'] || $aSearch['aAddressNonSearch']) $aSearch['iSearchRank'] += 1;
-                                        if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
-                                        /*
-                                        // Fall back to not searching for this item (better than nothing)
-                                        $aSearch = $aCurrentSearch;
-                                        $aSearch['iSearchRank'] += 1;
-                                        if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
-                                         */
-                                    }
-                                } elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null) {
-                                    // require a normalized exact match of the term
-                                    // if we have the normalizer version of the query
-                                    // available
-                                    if ($aSearch['sOperator'] === ''
-                                        && ($sNormQuery === null || !($aSearchTerm['word'] && strpos($sNormQuery, $aSearchTerm['word']) === false))) {
-                                        $aSearch['sClass'] = $aSearchTerm['class'];
-                                        $aSearch['sType'] = $aSearchTerm['type'];
-                                        if ($aSearchTerm['operator'] == '') {
-                                            $aSearch['sOperator'] = sizeof($aSearch['aName']) ? 'name' :  'near';
-                                            $aSearch['iSearchRank'] += 2;
-                                        } else {
-                                            $aSearch['sOperator'] = 'near'; // near = in for the moment
-                                        }
-
-                                        if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
-                                    }
-                                } elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) {
-                                    if (sizeof($aSearch['aName'])) {
-                                        if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strpos($sToken, ' ') !== false)) {
-                                            $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
-                                        } else {
-                                            $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
-                                            $aSearch['iSearchRank'] += 1000; // skip;
-                                        }
-                                    } else {
-                                        $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
-                                        //$aSearch['iNamePhrase'] = $iPhrase;
+                                // Recheck if the original word shows up in the query.
+                                $bWordInQuery = false;
+                                if (isset($aSearchTerm['word']) && $aSearchTerm['word']) {
+                                    $bWordInQuery = strpos(
+                                        $sNormQuery,
+                                        $this->normTerm($aSearchTerm['word'])
+                                    ) !== false;
+                                }
+                                $aNewSearches = $oCurrentSearch->extendWithFullTerm(
+                                    $aSearchTerm,
+                                    $bWordInQuery,
+                                    isset($aValidTokens[$sToken])
+                                      && strpos($sToken, ' ') === false,
+                                    $sPhraseType,
+                                    $iToken == 0 && $iPhrase == 0,
+                                    $iPhrase == 0,
+                                    $iToken + 1 == sizeof($aWordset)
+                                      && $iPhrase + 1 == sizeof($aPhrases),
+                                    $iGlobalRank
+                                );
+
+                                foreach ($aNewSearches as $oSearch) {
+                                    if ($oSearch->getRank() < $this->iMaxRank) {
+                                        $aNewWordsetSearches[] = $oSearch;
                                     }
-                                    if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
                                 }
                             }
                         }
                         // Look for partial matches.
                         // Note that there is no point in adding country terms here
-                        // because country are omitted in the address.
+                        // because country is omitted in the address.
                         if (isset($aValidTokens[$sToken]) && $sPhraseType != 'country') {
                             // Allow searching for a word - but at extra cost
                             foreach ($aValidTokens[$sToken] as $aSearchTerm) {
-                                if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) {
-                                    if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strpos($sToken, ' ') === false) {
-                                        $aSearch = $aCurrentSearch;
-                                        $aSearch['iSearchRank'] += 1;
-                                        if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency) {
-                                            $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
-                                            if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
-                                        } elseif (isset($aValidTokens[' '.$sToken])) { // revert to the token version?
-                                            $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
-                                            $aSearch['iSearchRank'] += 1;
-                                            if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
-                                            foreach ($aValidTokens[' '.$sToken] as $aSearchTermToken) {
-                                                if (empty($aSearchTermToken['country_code'])
-                                                    && empty($aSearchTermToken['lat'])
-                                                    && empty($aSearchTermToken['class'])
-                                                ) {
-                                                    $aSearch = $aCurrentSearch;
-                                                    $aSearch['iSearchRank'] += 1;
-                                                    $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
-                                                    if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
-                                                }
-                                            }
-                                        } else {
-                                            $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
-                                            if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
-                                            if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
-                                        }
-                                    }
-
-                                    if ((!$aCurrentSearch['sPostcode'] && !$aCurrentSearch['aAddress'] && !$aCurrentSearch['aAddressNonSearch'])
-                                        && (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase)) {
-                                        $aSearch = $aCurrentSearch;
-                                        $aSearch['iSearchRank'] += 1;
-                                        if (!sizeof($aCurrentSearch['aName'])) $aSearch['iSearchRank'] += 1;
-                                        if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
-                                        if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency) {
-                                            $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
-                                        } else {
-                                            $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
-                                        }
-                                        $aSearch['iNamePhrase'] = $iPhrase;
-                                        if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
+                                $aNewSearches = $oCurrentSearch->extendWithPartialTerm(
+                                    $aSearchTerm,
+                                    $bStructuredPhrases,
+                                    $iPhrase,
+                                    $aWordFrequencyScores,
+                                    isset($aValidTokens[' '.$sToken]) ? $aValidTokens[' '.$sToken] : array()
+                                );
+
+                                foreach ($aNewSearches as $oSearch) {
+                                    if ($oSearch->getRank() < $this->iMaxRank) {
+                                        $aNewWordsetSearches[] = $oSearch;
                                     }
                                 }
                             }
-                        } else {
-                            // Allow skipping a word - but at EXTREAM cost
-                            //$aSearch = $aCurrentSearch;
-                            //$aSearch['iSearchRank']+=100;
-                            //$aNewWordsetSearches[] = $aSearch;
                         }
                     }
                     // Sort and cut
-                    usort($aNewWordsetSearches, 'bySearchRank');
+                    usort($aNewWordsetSearches, array('Nominatim\SearchDescription', 'bySearchRank'));
                     $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
                 }
                 //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
 
                 $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
-                usort($aNewPhraseSearches, 'bySearchRank');
+                usort($aNewPhraseSearches, array('Nominatim\SearchDescription', 'bySearchRank'));
 
                 $aSearchHash = array();
                 foreach ($aNewPhraseSearches as $iSearch => $aSearch) {
@@ -904,9 +762,12 @@ class Geocode
             // Re-group the searches by their score, junk anything over 20 as just not worth trying
             $aGroupedSearches = array();
             foreach ($aNewPhraseSearches as $aSearch) {
-                if ($aSearch['iSearchRank'] < $this->iMaxRank) {
-                    if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
-                    $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
+                $iRank = $aSearch->getRank();
+                if ($iRank < $this->iMaxRank) {
+                    if (!isset($aGroupedSearches[$iRank])) {
+                        $aGroupedSearches[$iRank] = array();
+                    }
+                    $aGroupedSearches[$iRank][] = $aSearch;
                 }
             }
             ksort($aGroupedSearches);
@@ -922,16 +783,18 @@ class Geocode
             //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
         }
 
-        // Revisit searches, giving penalty to unlikely combinations
+        // Revisit searches, drop bad searches and give penalty to unlikely combinations.
         $aGroupedSearches = array();
-        foreach ($aSearches as $aSearch) {
-            if (!$aSearch['aName']) {
-                if ($aSearch['sHouseNumber']) {
-                    continue;
-                }
+        foreach ($aSearches as $oSearch) {
+            if (!$oSearch->isValidSearch($this->aCountryCodes)) {
+                continue;
+            }
+
+            $iRank = $oSearch->addToRank($iGlobalRank);
+            if (!isset($aGroupedSearches[$iRank])) {
+                $aGroupedSearches[$iRank] = array();
             }
-            $aSearch['iSearchRank'] += $iGlobalRank;
-            $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
+            $aGroupedSearches[$iRank][] = $oSearch;
         }
         ksort($aGroupedSearches);
 
@@ -974,13 +837,30 @@ class Geocode
     {
         if (!$this->sQuery && !$this->aStructuredQuery) return array();
 
-        $sNormQuery = $this->normTerm($this->sQuery);
-        $sLanguagePrefArraySQL = "ARRAY[".join(',', array_map("getDBQuoted", $this->aLangPrefOrder))."]";
-        $sCountryCodesSQL = false;
+        $oCtx = new SearchContext();
+
+        if ($this->aRoutePoints) {
+            $oCtx->setViewboxFromRoute(
+                $this->oDB,
+                $this->aRoutePoints,
+                $this->aRouteWidth,
+                $this->bBoundedSearch
+            );
+        } elseif ($this->aViewBox) {
+            $oCtx->setViewboxFromBox($this->aViewBox, $this->bBoundedSearch);
+        }
+        if ($this->aExcludePlaceIDs) {
+            $oCtx->setExcludeList($this->aExcludePlaceIDs);
+        }
         if ($this->aCountryCodes) {
-            $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
+            $oCtx->setCountryList($this->aCountryCodes);
         }
 
+        $sNormQuery = $this->normTerm($this->sQuery);
+        $sLanguagePrefArraySQL = getArraySQL(
+            array_map("getDBQuoted", $this->aLangPrefOrder)
+        );
+
         $sQuery = $this->sQuery;
         if (!preg_match('//u', $sQuery)) {
             userError("Query string is not UTF-8 encoded.");
@@ -993,101 +873,60 @@ class Geocode
             $sQuery = preg_replace('/(^|,)\s*la\s*(,|$)/', '\1louisiana\2', $sQuery);
         }
 
-        $bBoundingBoxSearch = $this->bBoundedSearch && $this->sViewboxSmallSQL;
-        if ($this->sViewboxCentreSQL) {
-            // For complex viewboxes (routes) precompute the bounding geometry
-            $sGeom = chksql(
-                $this->oDB->getOne("select ".$this->sViewboxSmallSQL),
-                "Could not get small viewbox"
-            );
-            $this->sViewboxSmallSQL = "'".$sGeom."'::geometry";
-
-            $sGeom = chksql(
-                $this->oDB->getOne("select ".$this->sViewboxLargeSQL),
-                "Could not get large viewbox"
-            );
-            $this->sViewboxLargeSQL = "'".$sGeom."'::geometry";
-        }
-
         // Do we have anything that looks like a lat/lon pair?
-        $oNearPoint = false;
-        if ($aLooksLike = NearPoint::extractFromQuery($sQuery)) {
-            $oNearPoint = $aLooksLike['pt'];
-            $sQuery = $aLooksLike['query'];
-        }
+        $sQuery = $oCtx->setNearPointFromQuery($sQuery);
 
         $aSearchResults = array();
         if ($sQuery || $this->aStructuredQuery) {
-            // Start with a blank search
-            $aSearches = array(
-                          array(
-                           'iSearchRank' => 0,
-                           'iNamePhrase' => -1,
-                           'sCountryCode' => false,
-                           'aName' => array(),
-                           'aAddress' => array(),
-                           'aFullNameAddress' => array(),
-                           'aNameNonSearch' => array(),
-                           'aAddressNonSearch' => array(),
-                           'sOperator' => '',
-                           'aFeatureName' => array(),
-                           'sClass' => '',
-                           'sType' => '',
-                           'sHouseNumber' => '',
-                           'sPostcode' => '',
-                           'oNear' => $oNearPoint
-                          )
-                         );
-
-            // Any 'special' terms in the search?
-            $bSpecialTerms = false;
-            preg_match_all('/\\[([\\w_]*)=([\\w_]*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
-            foreach ($aSpecialTermsRaw as $aSpecialTerm) {
-                $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
-                if (!$bSpecialTerms) {
-                    $aNewSearches = array();
-                    foreach ($aSearches as $aSearch) {
-                        $aNewSearch = $aSearch;
-                        $aNewSearch['sClass'] = $aSpecialTerm[1];
-                        $aNewSearch['sType'] = $aSpecialTerm[2];
-                        $aNewSearches[] = $aNewSearch;
-                    }
+            // Start with a single blank search
+            $aSearches = array(new SearchDescription($oCtx));
 
-                    $aSearches = $aNewSearches;
-                    $bSpecialTerms = true;
-                }
+            if ($sQuery) {
+                $sQuery = $aSearches[0]->extractKeyValuePairs($sQuery);
             }
 
-            preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
-            if (isset($this->aStructuredQuery['amenity']) && $this->aStructuredQuery['amenity']) {
-                $aSpecialTermsRaw[] = array('['.$this->aStructuredQuery['amenity'].']', $this->aStructuredQuery['amenity']);
+            $sSpecialTerm = '';
+            if ($sQuery) {
+                preg_match_all(
+                    '/\\[([\\w ]*)\\]/u',
+                    $sQuery,
+                    $aSpecialTermsRaw,
+                    PREG_SET_ORDER
+                );
+                foreach ($aSpecialTermsRaw as $aSpecialTerm) {
+                    $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
+                    if (!$sSpecialTerm) {
+                        $sSpecialTerm = $aSpecialTerm[1];
+                    }
+                }
+            }
+            if (!$sSpecialTerm && $this->aStructuredQuery
+                && isset($this->aStructuredQuery['amenity'])) {
+                $sSpecialTerm = $this->aStructuredQuery['amenity'];
                 unset($this->aStructuredQuery['amenity']);
             }
 
-            foreach ($aSpecialTermsRaw as $aSpecialTerm) {
-                $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
-                if ($bSpecialTerms) {
-                    continue;
-                }
-
-                $sToken = chksql($this->oDB->getOne("SELECT make_standard_name('".pg_escape_string($aSpecialTerm[1])."') AS string"));
-                $sSQL = 'SELECT * ';
-                $sSQL .= 'FROM ( ';
-                $sSQL .= '   SELECT word_id, word_token, word, class, type, country_code, operator';
-                $sSQL .= '   FROM word ';
+            if ($sSpecialTerm && !$aSearches[0]->hasOperator()) {
+                $sSpecialTerm = pg_escape_string($sSpecialTerm);
+                $sToken = chksql(
+                    $this->oDB->getOne("SELECT make_standard_name('$sSpecialTerm')"),
+                    "Cannot decode query. Wrong encoding?"
+                );
+                $sSQL = 'SELECT class, type FROM word ';
                 $sSQL .= '   WHERE word_token in (\' '.$sToken.'\')';
-                $sSQL .= ') AS x ';
-                $sSQL .= ' WHERE (class is not null AND class not in (\'place\'))';
+                $sSQL .= '   AND class is not null AND class not in (\'place\')';
                 if (CONST_Debug) var_Dump($sSQL);
                 $aSearchWords = chksql($this->oDB->getAll($sSQL));
                 $aNewSearches = array();
-                foreach ($aSearches as $aSearch) {
+                foreach ($aSearches as $oSearch) {
                     foreach ($aSearchWords as $aSearchTerm) {
-                        $aNewSearch = $aSearch;
-                        $aNewSearch['sClass'] = $aSearchTerm['class'];
-                        $aNewSearch['sType'] = $aSearchTerm['type'];
-                        $aNewSearches[] = $aNewSearch;
-                        $bSpecialTerms = true;
+                        $oNewSearch = clone $oSearch;
+                        $oNewSearch->setPoiSearch(
+                            Operator::TYPE,
+                            $aSearchTerm['class'],
+                            $aSearchTerm['type']
+                        );
+                        $aNewSearches[] = $oNewSearch;
                     }
                 }
                 $aSearches = $aNewSearches;
@@ -1179,18 +1018,14 @@ class Geocode
 
                 foreach ($aTokens as $sToken) {
                     // Unknown single word token with a number - assume it is a house number
-                    if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken, ' ') === false && preg_match('/[0-9]/', $sToken)) {
-                        $aValidTokens[' '.$sToken] = array(array('class' => 'place', 'type' => 'house'));
+                    if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken, ' ') === false && preg_match('/^[0-9]+$/', $sToken)) {
+                        $aValidTokens[' '.$sToken] = array(array('class' => 'place', 'type' => 'house', 'word_token' => ' '.$sToken));
                     }
                 }
 
                 // Any words that have failed completely?
                 // TODO: suggestions
 
-                // Start the search process
-                // array with: placeid => -1 | tiger-housenumber
-                $aResultPlaceIDs = array();
-
                 $aGroupedSearches = $this->getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases, $sNormQuery);
 
                 if ($this->bReverseInPlan) {
@@ -1207,10 +1042,10 @@ class Geocode
 
                     foreach ($aGroupedSearches as $aSearches) {
                         foreach ($aSearches as $aSearch) {
-                            if ($aSearch['iSearchRank'] < $this->iMaxRank) {
-                                if (!isset($aReverseGroupedSearches[$aSearch['iSearchRank']])) $aReverseGroupedSearches[$aSearch['iSearchRank']] = array();
-                                $aReverseGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
+                            if (!isset($aReverseGroupedSearches[$aSearch->getRank()])) {
+                                $aReverseGroupedSearches[$aSearch->getRank()] = array();
                             }
+                            $aReverseGroupedSearches[$aSearch->getRank()][] = $aSearch;
                         }
                     }
 
@@ -1221,38 +1056,9 @@ class Geocode
                 // Re-group the searches by their score, junk anything over 20 as just not worth trying
                 $aGroupedSearches = array();
                 foreach ($aSearches as $aSearch) {
-                    if ($aSearch['iSearchRank'] < $this->iMaxRank) {
-                        if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
-                        $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
-                    }
-                }
-                ksort($aGroupedSearches);
-            }
-
-            if (CONST_Search_TryDroppedAddressTerms && sizeof($this->aStructuredQuery) > 0) {
-                $aCopyGroupedSearches = $aGroupedSearches;
-                foreach ($aCopyGroupedSearches as $iGroup => $aSearches) {
-                    foreach ($aSearches as $iSearch => $aSearch) {
-                        $aReductionsList = array($aSearch['aAddress']);
-                        $iSearchRank = $aSearch['iSearchRank'];
-                        while (sizeof($aReductionsList) > 0) {
-                            $iSearchRank += 5;
-                            if ($iSearchRank > iMaxRank) break 3;
-                            $aNewReductionsList = array();
-                            foreach ($aReductionsList as $aReductionsWordList) {
-                                for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++) {
-                                    $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
-                                    $aReverseSearch = $aSearch;
-                                    $aSearch['aAddress'] = $aReductionsWordListResult;
-                                    $aSearch['iSearchRank'] = $iSearchRank;
-                                    $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
-                                    if (sizeof($aReductionsWordListResult) > 0) {
-                                        $aNewReductionsList[] = $aReductionsWordListResult;
-                                    }
-                                }
-                            }
-                            $aReductionsList = $aNewReductionsList;
-                        }
+                    if ($aSearch->getRank() < $this->iMaxRank) {
+                        if (!isset($aGroupedSearches[$aSearch->getRank()])) $aGroupedSearches[$aSearch->getRank()] = array();
+                        $aGroupedSearches[$aSearch->getRank()][] = $aSearch;
                     }
                 }
                 ksort($aGroupedSearches);
@@ -1274,479 +1080,38 @@ class Geocode
 
             if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
 
+            // Start the search process
+            // array with: placeid => -1 | tiger-housenumber
+            $aResultPlaceIDs = array();
             $iGroupLoop = 0;
             $iQueryLoop = 0;
             foreach ($aGroupedSearches as $iGroupedRank => $aSearches) {
                 $iGroupLoop++;
-                foreach ($aSearches as $aSearch) {
+                foreach ($aSearches as $oSearch) {
                     $iQueryLoop++;
-                    $searchedHousenumber = -1;
-
-                    if (CONST_Debug) echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>";
-                    if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
-
-                    if ($sCountryCodesSQL && $aSearch['sCountryCode'] && !in_array($aSearch['sCountryCode'], $this->aCountryCodes)) {
-                        continue;
-                    }
-
-                    // No location term?
-                    if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress'])) {
-                        if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber'] && !$aSearch['oNear']) {
-                            // Just looking for a country by code - look it up
-                            if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank) {
-                                $sSQL = "SELECT place_id FROM placex WHERE country_code='".$aSearch['sCountryCode']."' AND rank_search = 4";
-                                if ($bBoundingBoxSearch)
-                                    $sSQL .= " AND _st_intersects($this->sViewboxSmallSQL, geometry)";
-                                $sSQL .= " ORDER BY st_area(geometry) DESC LIMIT 1";
-                                if (CONST_Debug) var_dump($sSQL);
-                                $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
-                            } else {
-                                $aPlaceIDs = array();
-                            }
-                        } else {
-                            if (!$bBoundingBoxSearch && !$aSearch['oNear']) continue;
-                            if (!$aSearch['sClass']) continue;
-
-                            $sSQL = "SELECT COUNT(*) FROM pg_tables WHERE tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
-                            if (chksql($this->oDB->getOne($sSQL))) {
-                                $sSQL = "SELECT place_id FROM place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
-                                if ($sCountryCodesSQL) $sSQL .= " JOIN placex USING (place_id)";
-                                if ($aSearch['oNear']) {
-                                    $sSQL .= " WHERE ".$aSearch['oNear']->withinSQL('ct.centroid');
-                                } else {
-                                    $sSQL .= " WHERE st_contains($this->sViewboxSmallSQL, ct.centroid)";
-                                }
-                                if ($sCountryCodesSQL) $sSQL .= " AND country_code in ($sCountryCodesSQL)";
-                                if (sizeof($this->aExcludePlaceIDs)) {
-                                    $sSQL .= " AND place_id not in (".join(',', $this->aExcludePlaceIDs).")";
-                                }
-                                if ($this->sViewboxCentreSQL) {
-                                    $sSQL .= " ORDER BY ST_Distance($this->sViewboxCentreSQL, ct.centroid) ASC";
-                                } elseif ($aSearch['oNear']) {
-                                    $sSQL .= " ORDER BY ".$aSearch['oNear']->distanceSQL('ct.centroid').' ASC';
-                                }
-                                $sSQL .= " limit $this->iLimit";
-                                if (CONST_Debug) var_dump($sSQL);
-                                $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
-                            } else if ($aSearch['oNear']) {
-                                $sSQL = "SELECT place_id ";
-                                $sSQL .= "FROM placex ";
-                                $sSQL .= "WHERE class='".$aSearch['sClass']."' ";
-                                $sSQL .= "  AND type='".$aSearch['sType']."'";
-                                $sSQL .= "  AND ".$aSearch['oNear']->withinSQL('geometry');
-                                $sSQL .= "  AND linked_place_id is null";
-                                if ($sCountryCodesSQL) $sSQL .= " AND country_code in ($sCountryCodesSQL)";
-                                $sSQL .= " ORDER BY ".$aSearch['oNear']->distanceSQL('centroid')." ASC";
-                                $sSQL .= " LIMIT $this->iLimit";
-                                if (CONST_Debug) var_dump($sSQL);
-                                $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
-                            }
-                        }
-                    } elseif ($aSearch['oNear'] && !sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['sClass']) {
-                        // If a coordinate is given, the search must either
-                        // be for a name or a special search. Ignore everythin else.
-                        $aPlaceIDs = array();
-                    } elseif ($aSearch['sOperator'] == 'postcode') {
-                        $sSQL  = "SELECT p.place_id FROM location_postcode p ";
-                        if (sizeof($aSearch['aAddress'])) {
-                            $sSQL .= ", search_name s ";
-                            $sSQL .= "WHERE s.place_id = p.parent_place_id ";
-                            $sSQL .= "AND array_cat(s.nameaddress_vector, s.name_vector) @> ARRAY[".join($aSearch['aAddress'], ",")."] AND ";
-                        } else {
-                            $sSQL .= " WHERE ";
-                        }
-                        $sSQL .= "p.postcode = '".pg_escape_string(reset($aSearch['aName']))."'";
-                        if ($aSearch['sCountryCode']) {
-                            $sSQL .= " AND p.country_code = '".$aSearch['sCountryCode']."'";
-                        } elseif ($sCountryCodesSQL) {
-                            $sSQL .= " AND p.country_code in ($sCountryCodesSQL)";
-                        }
-                        $sSQL .= " LIMIT $this->iLimit";
-                        if (CONST_Debug) var_dump($sSQL);
-                        $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
-                    } else {
-                        $aPlaceIDs = array();
-
-                        // First we need a position, either aName or fLat or both
-                        $aTerms = array();
-                        $aOrder = array();
-
-                        if ($aSearch['sHouseNumber'] && sizeof($aSearch['aAddress'])) {
-                            $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M';
-                            $aOrder[] = "";
-                            $aOrder[0] = "  (";
-                            $aOrder[0] .= "   EXISTS(";
-                            $aOrder[0] .= "     SELECT place_id ";
-                            $aOrder[0] .= "     FROM placex ";
-                            $aOrder[0] .= "     WHERE parent_place_id = search_name.place_id";
-                            $aOrder[0] .= "       AND transliteration(housenumber) ~* E'".$sHouseNumberRegex."' ";
-                            $aOrder[0] .= "     LIMIT 1";
-                            $aOrder[0] .= "   ) ";
-                            // also housenumbers from interpolation lines table are needed
-                            $aOrder[0] .= "   OR EXISTS(";
-                            $aOrder[0] .= "     SELECT place_id ";
-                            $aOrder[0] .= "     FROM location_property_osmline ";
-                            $aOrder[0] .= "     WHERE parent_place_id = search_name.place_id";
-                            $aOrder[0] .= "       AND startnumber is not NULL";
-                            $aOrder[0] .= "       AND ".intval($aSearch['sHouseNumber']).">=startnumber ";
-                            $aOrder[0] .= "       AND ".intval($aSearch['sHouseNumber'])."<=endnumber ";
-                            $aOrder[0] .= "     LIMIT 1";
-                            $aOrder[0] .= "   )";
-                            $aOrder[0] .= " )";
-                            $aOrder[0] .= " DESC";
-                        }
-
-                        // TODO: filter out the pointless search terms (2 letter name tokens and less)
-                        // they might be right - but they are just too darned expensive to run
-                        if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'], ",")."]";
-                        if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'], ",")."]";
-                        if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress']) {
-                            // For infrequent name terms disable index usage for address
-                            if (CONST_Search_NameOnlySearchFrequencyThreshold
-                                && sizeof($aSearch['aName']) == 1
-                                && $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold
-                            ) {
-                                $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'], $aSearch['aAddressNonSearch']), ",")."]";
-                            } else {
-                                $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'], ",")."]";
-                                if (sizeof($aSearch['aAddressNonSearch'])) {
-                                    $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'], ",")."]";
-                                }
-                            }
-                        }
-                        if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
-                        if ($aSearch['sHouseNumber']) {
-                            $aTerms[] = "address_rank between 16 and 27";
-                        } elseif (!$aSearch['sClass'] || $aSearch['sOperator'] == 'name') {
-                            if ($this->iMinAddressRank > 0) {
-                                $aTerms[] = "address_rank >= ".$this->iMinAddressRank;
-                            }
-                            if ($this->iMaxAddressRank < 30) {
-                                $aTerms[] = "address_rank <= ".$this->iMaxAddressRank;
-                            }
-                        }
-                        if ($aSearch['oNear']) {
-                            $aTerms[] = $aSearch['oNear']->withinSQL('centroid');
-
-                            $aOrder[] = $aSearch['oNear']->distanceSQL('centroid');
-                        } elseif ($aSearch['sPostcode']) {
-                            if (!sizeof($aSearch['aAddress'])) {
-                                $aTerms[] = "EXISTS(SELECT place_id FROM location_postcode p WHERE p.postcode = '".$aSearch['sPostcode']."' AND ST_DWithin(search_name.centroid, p.geometry, 0.1))";
-                            } else {
-                                $aOrder[] = "(SELECT min(ST_Distance(search_name.centroid, p.geometry)) FROM location_postcode p WHERE p.postcode = '".$aSearch['sPostcode']."')";
-                            }
-                        }
-                        if (sizeof($this->aExcludePlaceIDs)) {
-                            $aTerms[] = "place_id not in (".join(',', $this->aExcludePlaceIDs).")";
-                        }
-                        if ($sCountryCodesSQL) {
-                            $aTerms[] = "country_code in ($sCountryCodesSQL)";
-                        }
-
-                        if ($bBoundingBoxSearch) $aTerms[] = "centroid && $this->sViewboxSmallSQL";
-                        if ($oNearPoint) {
-                            $aOrder[] = $oNearPoint->distanceSQL('centroid');
-                        }
-
-                        if ($aSearch['sHouseNumber']) {
-                            $sImportanceSQL = '- abs(26 - address_rank) + 3';
-                        } else {
-                            $sImportanceSQL = '(CASE WHEN importance = 0 OR importance IS NULL THEN 0.75-(search_rank::float/40) ELSE importance END)';
-                        }
-                        if ($this->sViewboxSmallSQL) $sImportanceSQL .= " * CASE WHEN ST_Contains($this->sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
-                        if ($this->sViewboxLargeSQL) $sImportanceSQL .= " * CASE WHEN ST_Contains($this->sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
-
-                        $aOrder[] = "$sImportanceSQL DESC";
-                        if (sizeof($aSearch['aFullNameAddress'])) {
-                            $sExactMatchSQL = ' ( ';
-                            $sExactMatchSQL .= '   SELECT count(*) FROM ( ';
-                            $sExactMatchSQL .= '      SELECT unnest(ARRAY['.join($aSearch['aFullNameAddress'], ",").']) ';
-                            $sExactMatchSQL .= '      INTERSECT ';
-                            $sExactMatchSQL .= '      SELECT unnest(nameaddress_vector)';
-                            $sExactMatchSQL .= '   ) s';
-                            $sExactMatchSQL .= ') as exactmatch';
-                            $aOrder[] = 'exactmatch DESC';
-                        } else {
-                            $sExactMatchSQL = '0::int as exactmatch';
-                        }
-
-                        if (sizeof($aTerms)) {
-                            $sSQL = "SELECT place_id, ";
-                            $sSQL .= $sExactMatchSQL;
-                            $sSQL .= " FROM search_name";
-                            $sSQL .= " WHERE ".join(' and ', $aTerms);
-                            $sSQL .= " ORDER BY ".join(', ', $aOrder);
-                            if ($aSearch['sHouseNumber'] || $aSearch['sClass']) {
-                                $sSQL .= " LIMIT 20";
-                            } elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass']) {
-                                $sSQL .= " LIMIT 1";
-                            } else {
-                                $sSQL .= " LIMIT ".$this->iLimit;
-                            }
-
-                            if (CONST_Debug) var_dump($sSQL);
-                            $aViewBoxPlaceIDs = chksql(
-                                $this->oDB->getAll($sSQL),
-                                "Could not get places for search terms."
-                            );
-                            //var_dump($aViewBoxPlaceIDs);
-                            // Did we have an viewbox matches?
-                            $aPlaceIDs = array();
-                            $bViewBoxMatch = false;
-                            foreach ($aViewBoxPlaceIDs as $aViewBoxRow) {
-                                //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
-                                //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
-                                //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
-                                //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
-                                $aPlaceIDs[] = $aViewBoxRow['place_id'];
-                                $this->exactMatchCache[$aViewBoxRow['place_id']] = $aViewBoxRow['exactmatch'];
-                            }
-                        }
-                        //var_Dump($aPlaceIDs);
-                        //exit;
-
-                        //now search for housenumber, if housenumber provided
-                        if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs)) {
-                            $searchedHousenumber = intval($aSearch['sHouseNumber']);
-                            $aRoadPlaceIDs = $aPlaceIDs;
-                            $sPlaceIDs = join(',', $aPlaceIDs);
-
-                            // Now they are indexed, look for a house attached to a street we found
-                            $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M';
-                            $sSQL = "SELECT place_id FROM placex ";
-                            $sSQL .= "WHERE parent_place_id in (".$sPlaceIDs.") and transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
-                            if (sizeof($this->aExcludePlaceIDs)) {
-                                $sSQL .= " AND place_id not in (".join(',', $this->aExcludePlaceIDs).")";
-                            }
-                            $sSQL .= " LIMIT $this->iLimit";
-                            if (CONST_Debug) var_dump($sSQL);
-                            $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
-
-                            // if nothing found, search in the interpolation line table
-                            if (!sizeof($aPlaceIDs)) {
-                                // do we need to use transliteration and the regex for housenumbers???
-                                //new query for lines, not housenumbers anymore
-                                $sSQL = "SELECT distinct place_id FROM location_property_osmline";
-                                $sSQL .= " WHERE startnumber is not NULL and parent_place_id in (".$sPlaceIDs.") and (";
-                                if ($searchedHousenumber%2 == 0) {
-                                    //if housenumber is even, look for housenumber in streets with interpolationtype even or all
-                                    $sSQL .= "interpolationtype='even'";
-                                } else {
-                                    //look for housenumber in streets with interpolationtype odd or all
-                                    $sSQL .= "interpolationtype='odd'";
-                                }
-                                $sSQL .= " or interpolationtype='all') and ";
-                                $sSQL .= $searchedHousenumber.">=startnumber and ";
-                                $sSQL .= $searchedHousenumber."<=endnumber";
-
-                                if (sizeof($this->aExcludePlaceIDs)) {
-                                    $sSQL .= " AND place_id not in (".join(',', $this->aExcludePlaceIDs).")";
-                                }
-                                //$sSQL .= " limit $this->iLimit";
-                                if (CONST_Debug) var_dump($sSQL);
-                                //get place IDs
-                                $aPlaceIDs = chksql($this->oDB->getCol($sSQL, 0));
-                            }
-
-                            // If nothing found try the aux fallback table
-                            if (CONST_Use_Aux_Location_data && !sizeof($aPlaceIDs)) {
-                                $sSQL = "SELECT place_id FROM location_property_aux ";
-                                $sSQL .= " WHERE parent_place_id in (".$sPlaceIDs.") ";
-                                $sSQL .= " AND housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
-                                if (sizeof($this->aExcludePlaceIDs)) {
-                                    $sSQL .= " AND parent_place_id not in (".join(',', $this->aExcludePlaceIDs).")";
-                                }
-                                //$sSQL .= " limit $this->iLimit";
-                                if (CONST_Debug) var_dump($sSQL);
-                                $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
-                            }
-
-                            //if nothing was found in placex or location_property_aux, then search in Tiger data for this housenumber(location_property_tiger)
-                            if (CONST_Use_US_Tiger_Data && !sizeof($aPlaceIDs)) {
-                                $sSQL = "SELECT distinct place_id FROM location_property_tiger";
-                                $sSQL .= " WHERE parent_place_id in (".$sPlaceIDs.") and (";
-                                if ($searchedHousenumber%2 == 0) {
-                                    $sSQL .= "interpolationtype='even'";
-                                } else {
-                                    $sSQL .= "interpolationtype='odd'";
-                                }
-                                $sSQL .= " or interpolationtype='all') and ";
-                                $sSQL .= $searchedHousenumber.">=startnumber and ";
-                                $sSQL .= $searchedHousenumber."<=endnumber";
-
-                                if (sizeof($this->aExcludePlaceIDs)) {
-                                    $sSQL .= " AND place_id not in (".join(',', $this->aExcludePlaceIDs).")";
-                                }
-                                //$sSQL .= " limit $this->iLimit";
-                                if (CONST_Debug) var_dump($sSQL);
-                                //get place IDs
-                                $aPlaceIDs = chksql($this->oDB->getCol($sSQL, 0));
-                            }
-
-                            // Fallback to the road (if no housenumber was found)
-                            if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber'])
-                                && ($aSearch['aAddress'] || $aSearch['sCountryCode'])) {
-                                $aPlaceIDs = $aRoadPlaceIDs;
-                                //set to -1, if no housenumbers were found
-                                $searchedHousenumber = -1;
-                            }
-                            //else: housenumber was found, remains saved in searchedHousenumber
-                        }
-
-
-                        if ($aSearch['sClass'] && sizeof($aPlaceIDs)) {
-                            $sPlaceIDs = join(',', $aPlaceIDs);
-                            $aClassPlaceIDs = array();
-
-                            if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name') {
-                                // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
-                                $sSQL = "SELECT place_id ";
-                                $sSQL .= " FROM placex ";
-                                $sSQL .= " WHERE place_id in ($sPlaceIDs) ";
-                                $sSQL .= "   AND class='".$aSearch['sClass']."' ";
-                                $sSQL .= "   AND type='".$aSearch['sType']."'";
-                                $sSQL .= "   AND linked_place_id is null";
-                                if ($sCountryCodesSQL) $sSQL .= " AND country_code in ($sCountryCodesSQL)";
-                                $sSQL .= " ORDER BY rank_search ASC ";
-                                $sSQL .= " LIMIT $this->iLimit";
-                                if (CONST_Debug) var_dump($sSQL);
-                                $aClassPlaceIDs = chksql($this->oDB->getCol($sSQL));
-                            }
-
-                            if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') { // & in
-                                $sClassTable = 'place_classtype_'.$aSearch['sClass'].'_'.$aSearch['sType'];
-                                $sSQL = "SELECT count(*) FROM pg_tables ";
-                                $sSQL .= "WHERE tablename = '$sClassTable'";
-                                $bCacheTable = chksql($this->oDB->getOne($sSQL));
-
-                                $sSQL = "SELECT min(rank_search) FROM placex WHERE place_id in ($sPlaceIDs)";
-
-                                if (CONST_Debug) var_dump($sSQL);
-                                $this->iMaxRank = ((int)chksql($this->oDB->getOne($sSQL)));
-
-                                // For state / country level searches the normal radius search doesn't work very well
-                                $sPlaceGeom = false;
-                                if ($this->iMaxRank < 9 && $bCacheTable) {
-                                    // Try and get a polygon to search in instead
-                                    $sSQL = "SELECT geometry ";
-                                    $sSQL .= " FROM placex";
-                                    $sSQL .= " WHERE place_id in ($sPlaceIDs)";
-                                    $sSQL .= "   AND rank_search < $this->iMaxRank + 5";
-                                    $sSQL .= "   AND ST_Geometrytype(geometry) in ('ST_Polygon','ST_MultiPolygon')";
-                                    $sSQL .= " ORDER BY rank_search ASC ";
-                                    $sSQL .= " LIMIT 1";
-                                    if (CONST_Debug) var_dump($sSQL);
-                                    $sPlaceGeom = chksql($this->oDB->getOne($sSQL));
-                                }
-
-                                if ($sPlaceGeom) {
-                                    $sPlaceIDs = false;
-                                } else {
-                                    $this->iMaxRank += 5;
-                                    $sSQL = "SELECT place_id FROM placex WHERE place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
-                                    if (CONST_Debug) var_dump($sSQL);
-                                    $aPlaceIDs = chksql($this->oDB->getCol($sSQL));
-                                    $sPlaceIDs = join(',', $aPlaceIDs);
-                                }
-
-                                if ($sPlaceIDs || $sPlaceGeom) {
-                                    $fRange = 0.01;
-                                    if ($bCacheTable) {
-                                        // More efficient - can make the range bigger
-                                        $fRange = 0.05;
-
-                                        $sOrderBySQL = '';
-                                        if ($oNearPoint) {
-                                            $sOrderBySQL = $oNearPoint->distanceSQL('l.centroid');
-                                        } elseif ($sPlaceIDs) {
-                                            $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
-                                        } elseif ($sPlaceGeom) {
-                                            $sOrderBySQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
-                                        }
-
-                                        $sSQL = "select distinct i.place_id".($sOrderBySQL?', i.order_term':'')." from (";
-                                        $sSQL .= "select l.place_id".($sOrderBySQL?','.$sOrderBySQL.' as order_term':'')." from ".$sClassTable." as l";
-                                        if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
-                                        if ($sPlaceIDs) {
-                                            $sSQL .= ",placex as f where ";
-                                            $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
-                                        }
-                                        if ($sPlaceGeom) {
-                                            $sSQL .= " where ";
-                                            $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
-                                        }
-                                        if (sizeof($this->aExcludePlaceIDs)) {
-                                            $sSQL .= " and l.place_id not in (".join(',', $this->aExcludePlaceIDs).")";
-                                        }
-                                        if ($sCountryCodesSQL) $sSQL .= " and lp.country_code in ($sCountryCodesSQL)";
-                                        $sSQL .= 'limit 300) i ';
-                                        if ($sOrderBySQL) $sSQL .= "order by order_term asc";
-                                        if ($this->iOffset) $sSQL .= " offset $this->iOffset";
-                                        $sSQL .= " limit $this->iLimit";
-                                        if (CONST_Debug) var_dump($sSQL);
-                                        $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($this->oDB->getCol($sSQL)));
-                                    } else {
-                                        if ($aSearch['oNear']) {
-                                            $fRange = $aSearch['oNear']->radius();
-                                        }
-
-                                        $sOrderBySQL = '';
-                                        if ($oNearPoint) {
-                                            $sOrderBySQL = $oNearPoint->distanceSQL('l.geometry');
-                                        } else {
-                                            $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
-                                        }
-
-                                        $sSQL = "SELECT distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'');
-                                        $sSQL .= " FROM placex as l, placex as f ";
-                                        $sSQL .= " WHERE f.place_id in ($sPlaceIDs) ";
-                                        $sSQL .= "  AND ST_DWithin(l.geometry, f.centroid, $fRange) ";
-                                        $sSQL .= "  AND l.class='".$aSearch['sClass']."' ";
-                                        $sSQL .= "  AND l.type='".$aSearch['sType']."' ";
-                                        if (sizeof($this->aExcludePlaceIDs)) {
-                                            $sSQL .= " AND l.place_id not in (".join(',', $this->aExcludePlaceIDs).")";
-                                        }
-                                        if ($sCountryCodesSQL) $sSQL .= " AND l.country_code in ($sCountryCodesSQL)";
-                                        if ($sOrderBySQL) $sSQL .= "ORDER BY ".$sOrderBySQL." ASC";
-                                        if ($this->iOffset) $sSQL .= " OFFSET $this->iOffset";
-                                        $sSQL .= " limit $this->iLimit";
-                                        if (CONST_Debug) var_dump($sSQL);
-                                        $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($this->oDB->getCol($sSQL)));
-                                    }
-                                }
-                            }
-                            $aPlaceIDs = $aClassPlaceIDs;
-                        }
-                    }
 
                     if (CONST_Debug) {
-                        echo "<br><b>Place IDs:</b> ";
-                        var_Dump($aPlaceIDs);
+                        echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>";
+                        _debugDumpGroupedSearches(array($iGroupedRank => array($oSearch)), $aValidTokens);
                     }
 
-                    if (sizeof($aPlaceIDs) && $aSearch['sPostcode']) {
-                        $sSQL = 'SELECT place_id FROM placex';
-                        $sSQL .= ' WHERE place_id in ('.join(',', $aPlaceIDs).')';
-                        $sSQL .= " AND postcode = '".pg_escape_string($aSearch['sPostcode'])."'";
-                        if (CONST_Debug) var_dump($sSQL);
-                        $aFilteredPlaceIDs = chksql($this->oDB->getCol($sSQL));
-                        if ($aFilteredPlaceIDs) {
-                            $aPlaceIDs = $aFilteredPlaceIDs;
-                            if (CONST_Debug) {
-                                echo "<br><b>Place IDs after postcode filtering:</b> ";
-                                var_Dump($aPlaceIDs);
-                            }
-                        }
-                    }
+                    $aRes = $oSearch->query(
+                        $this->oDB,
+                        $aWordFrequencyScores,
+                        $this->exactMatchCache,
+                        $this->iMinAddressRank,
+                        $this->iMaxAddressRank,
+                        $this->iLimit
+                    );
 
-                    foreach ($aPlaceIDs as $iPlaceID) {
+                    foreach ($aRes['IDs'] as $iPlaceID) {
                         // array for placeID => -1 | Tiger housenumber
-                        $aResultPlaceIDs[$iPlaceID] = $searchedHousenumber;
+                        $aResultPlaceIDs[$iPlaceID] = $aRes['houseNumber'];
                     }
                     if ($iQueryLoop > 20) break;
                 }
 
-                if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30)) {
+                if (sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30)) {
                     // Need to verify passes rank limits before dropping out of the loop (yuk!)
                     // reduces the number of place ids, like a filter
                     // rank_address is 30 for interpolated housenumbers
@@ -1789,31 +1154,26 @@ class Geocode
                     $aResultPlaceIDs = $tempIDs;
                 }
 
-                //exit;
-                if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
+                if (sizeof($aResultPlaceIDs)) break;
                 if ($iGroupLoop > 4) break;
                 if ($iQueryLoop > 30) break;
             }
 
             // Did we find anything?
-            if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) {
-                $aSearchResults = $this->getDetails($aResultPlaceIDs);
+            if (sizeof($aResultPlaceIDs)) {
+                $aSearchResults = $this->getDetails($aResultPlaceIDs, $oCtx);
             }
         } else {
             // Just interpret as a reverse geocode
             $oReverse = new ReverseGeocode($this->oDB);
             $oReverse->setZoom(18);
 
-            $aLookup = $oReverse->lookup(
-                $oNearPoint->lat(),
-                $oNearPoint->lon(),
-                false
-            );
+            $aLookup = $oReverse->lookupPoint($oCtx->sqlNear, false);
 
             if (CONST_Debug) var_dump("Reverse search", $aLookup);
 
             if ($aLookup['place_id']) {
-                $aSearchResults = $this->getDetails(array($aLookup['place_id'] => -1));
+                $aSearchResults = $this->getDetails(array($aLookup['place_id'] => -1), $oCtx);
                 $aResultPlaceIDs[$aLookup['place_id']] = -1;
             } else {
                 $aSearchResults = array();
diff --git a/lib/NearPoint.php b/lib/NearPoint.php
deleted file mode 100644 (file)
index 30845b7..0000000
+++ /dev/null
@@ -1,158 +0,0 @@
-<?php
-
-namespace Nominatim;
-
-/**
- * A geographic point with a search radius.
- */
-class NearPoint
-{
-    private $fLat;
-    private $fLon;
-    private $fRadius;
-
-    private $sSQL;
-
-
-    public function __construct($lat, $lon, $radius = 0.1)
-    {
-        $this->fLat = (float)$lat;
-        $this->fLon = (float)$lon;
-        $this->fRadius = (float)$radius;
-        $this->sSQL = 'ST_SetSRID(ST_Point('.$this->fLon.','.$this->fLat.'),4326)';
-    }
-
-    public function lat()
-    {
-        return $this->fLat;
-    }
-
-    public function lon()
-    {
-        return $this->fLon;
-    }
-
-    public function radius()
-    {
-        return $this->fRadius;
-    }
-
-    public function distanceSQL($sObj)
-    {
-        return 'ST_Distance('.$this->sSQL.", $sObj)";
-    }
-
-    public function withinSQL($sObj)
-    {
-        return sprintf('ST_DWithin(%s, %s, %F)', $sObj, $this->sSQL, $this->fRadius);
-    }
-
-    /**
-     * Check that the coordinates are valid WSG84 coordinates.
-     *
-     * @return bool True if the coordinates are correctly bounded.
-     */
-    public function isValid()
-    {
-        return ($this->fLat <= 90.1
-                && $this->fLat >= -90.1
-                && $this->fLon <= 180.1
-                && $this->fLon >= -180.1);
-    }
-
-    /**
-     * Extract a coordinate point from a query string.
-     *
-     * If a coordinate is found an array of a new NearPoint and the
-     * remaining query is returned or false otherwise.
-     *
-     * @param string $sQuery Query to scan.
-     *
-     * @return array|false If a coordinate was found, an array with
-     *                     `pt` as the NearPoint coordinates and `query`
-     *                      with the remaining query string. False otherwiese.
-     */
-    public static function extractFromQuery($sQuery)
-    {
-        // Do we have anything that looks like a lat/lon pair?
-        // returns array(lat,lon,query_with_lat_lon_removed)
-        // or null
-        $sFound    = null;
-        $fQueryLat = null;
-        $fQueryLon = null;
-
-        if (preg_match('/\\s*([NS])[ ]+([0-9]+[0-9.]*)[° ]+([0-9.]+)?[′\']*[, ]+([EW])[ ]+([0-9]+)[° ]+([0-9]+[0-9.]*)[′\']*\\s*/', $sQuery, $aData)) {
-            /*               1         2                   3                    4         5            6
-             * degrees decimal minutes
-             * N 40 26.767, W 79 58.933
-             * N 40°26.767′, W 79°58.933′
-             */
-            $sFound    = $aData[0];
-            $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2] + $aData[3]/60);
-            $fQueryLon = ($aData[4]=='E'?1:-1) * ($aData[5] + $aData[6]/60);
-        } elseif (preg_match('/\\s*([0-9]+)[° ]+([0-9]+[0-9.]*)?[′\']*[ ]+([NS])[, ]+([0-9]+)[° ]+([0-9]+[0-9.]*)?[′\' ]+([EW])\\s*/', $sQuery, $aData)) {
-            /*                     1            2                         3          4            5                      6
-             * degrees decimal minutes
-             * 40 26.767 N, 79 58.933 W
-             * 40° 26.767′ N 79° 58.933′ W
-             */
-            $sFound    = $aData[0];
-            $fQueryLat = ($aData[3]=='N'?1:-1) * ($aData[1] + $aData[2]/60);
-            $fQueryLon = ($aData[6]=='E'?1:-1) * ($aData[4] + $aData[5]/60);
-        } elseif (preg_match('/\\s*([NS])[ ]([0-9]+)[° ]+([0-9]+)[′\' ]+([0-9]+)[″"]*[, ]+([EW])[ ]([0-9]+)[° ]+([0-9]+)[′\' ]+([0-9]+)[″"]*\\s*/', $sQuery, $aData)) {
-            /*                     1        2            3              4                 5        6            7              8
-             * degrees decimal seconds
-             * N 40 26 46 W 79 58 56
-             * N 40° 26′ 46″, W 79° 58′ 56″
-             */
-            $sFound    = $aData[0];
-            $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2] + $aData[3]/60 + $aData[4]/3600);
-            $fQueryLon = ($aData[5]=='E'?1:-1) * ($aData[6] + $aData[7]/60 + $aData[8]/3600);
-        } elseif (preg_match('/\\s*([0-9]+)[° ]+([0-9]+)[′\' ]+([0-9]+)[″" ]+([NS])[, ]+([0-9]+)[° ]+([0-9]+)[′\' ]+([0-9]+)[″" ]+([EW])\\s*/', $sQuery, $aData)) {
-            /*                     1            2              3             4          5            6              7             8
-             * degrees decimal seconds
-             * 40 26 46 N 79 58 56 W
-             * 40° 26′ 46″ N, 79° 58′ 56″ W
-             */
-            $sFound    = $aData[0];
-            $fQueryLat = ($aData[4]=='N'?1:-1) * ($aData[1] + $aData[2]/60 + $aData[3]/3600);
-            $fQueryLon = ($aData[8]=='E'?1:-1) * ($aData[5] + $aData[6]/60 + $aData[7]/3600);
-        } elseif (preg_match('/\\s*([NS])[ ]([0-9]+[0-9]*\\.[0-9]+)[°]*[, ]+([EW])[ ]([0-9]+[0-9]*\\.[0-9]+)[°]*\\s*/', $sQuery, $aData)) {
-            /*                     1        2                               3        4
-             * degrees decimal
-             * N 40.446° W 79.982°
-             */
-            $sFound    = $aData[0];
-            $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2]);
-            $fQueryLon = ($aData[3]=='E'?1:-1) * ($aData[4]);
-        } elseif (preg_match('/\\s*([0-9]+[0-9]*\\.[0-9]+)[° ]+([NS])[, ]+([0-9]+[0-9]*\\.[0-9]+)[° ]+([EW])\\s*/', $sQuery, $aData)) {
-            /*                     1                           2          3                           4
-             * degrees decimal
-             * 40.446° N 79.982° W
-             */
-            $sFound    = $aData[0];
-            $fQueryLat = ($aData[2]=='N'?1:-1) * ($aData[1]);
-            $fQueryLon = ($aData[4]=='E'?1:-1) * ($aData[3]);
-        } elseif (preg_match('/(\\s*\\[|^\\s*|\\s*)(-?[0-9]+[0-9]*\\.[0-9]+)[, ]+(-?[0-9]+[0-9]*\\.[0-9]+)(\\]\\s*|\\s*$|\\s*)/', $sQuery, $aData)) {
-            /*                 1                   2                             3                        4
-             * degrees decimal
-             * 12.34, 56.78
-             * 12.34 56.78
-             * [12.456,-78.90]
-             */
-            $sFound    = $aData[0];
-            $fQueryLat = $aData[2];
-            $fQueryLon = $aData[3];
-        } else {
-            return false;
-        }
-
-        $oPt = new NearPoint($fQueryLat, $fQueryLon);
-
-        if (!$oPt->isValid()) return false;
-
-        $sQuery = trim(str_replace($sFound, ' ', $sQuery));
-
-        return array('pt' => $oPt, 'query' => $sQuery);
-    }
-}
index 1de0893c4a7b46fcc886b6d5d3bf9d7d9262558e..9b43a3e38cdd8d2aabd8c5391b0aeee90898631b 100644 (file)
@@ -66,15 +66,22 @@ class ReverseGeocode
         );
     }
 
+    public function lookup($fLat, $fLon, $bDoInterpolation = true)
+    {
+        return $this->lookupPoint(
+            'ST_SetSRID(ST_Point('.$fLon.','.$fLat.'),4326)',
+            $bDoInterpolation
+        );
+    }
+
     /* lookup()
      * returns { place_id =>, type => '(osm|tiger)' }
      * fails if no place was found
      */
 
 
-    public function lookup($fLat, $fLon, $bDoInterpolation = true)
+    public function lookupPoint($sPointSQL, $bDoInterpolation = true)
     {
-        $sPointSQL = 'ST_SetSRID(ST_Point('.$fLon.','.$fLat.'),4326)';
         $iMaxRank = $this->iMaxRank;
 
         // Find the nearest point
diff --git a/lib/SearchContext.php b/lib/SearchContext.php
new file mode 100644 (file)
index 0000000..134b138
--- /dev/null
@@ -0,0 +1,270 @@
+<?php
+
+namespace Nominatim;
+
+require_once(CONST_BasePath.'/lib/lib.php');
+
+
+/**
+ * Collection of search constraints that are independent of the
+ * actual interpretation of the search query.
+ *
+ * The search context is shared between all SearchDescriptions. This
+ * object mainly serves as context provider for the database queries.
+ * Therefore most data is directly cached as SQL statements.
+ */
+class SearchContext
+{
+    /// Search radius around a given Near reference point.
+    private $fNearRadius = false;
+    /// True if search must be restricted to viewbox only.
+    public $bViewboxBounded = false;
+
+    /// Reference point for search (as SQL).
+    public $sqlNear = '';
+    /// Viewbox selected for search (as SQL).
+    public $sqlViewboxSmall = '';
+    /// Viewbox with a larger buffer around (as SQL).
+    public $sqlViewboxLarge = '';
+    /// Reference along a route (as SQL).
+    public $sqlViewboxCentre = '';
+    /// List of countries to restrict search to (as SQL).
+    public $sqlCountryList = '';
+    /// List of place IDs to exclude (as SQL).
+    private $sqlExcludeList = '';
+
+
+    /**
+     * Check if a reference point is defined.
+     *
+     * @return bool True if a reference point is defined.
+     */
+    public function hasNearPoint()
+    {
+        return $this->fNearRadius !== false;
+    }
+
+    /**
+     * Get radius around reference point.
+     *
+     * @return float Search radius around refernce point.
+     */
+    public function nearRadius()
+    {
+        return $this->fNearRadius;
+    }
+
+    /**
+     * Set search reference point in WGS84.
+     *
+     * If set, then only places around this point will be taken into account.
+     *
+     * @param float $fLat    Latitude of point.
+     * @param float $fLon    Longitude of point.
+     * @param float $fRadius Search radius around point.
+     *
+     * @return void
+     */
+    public function setNearPoint($fLat, $fLon, $fRadius = 0.1)
+    {
+        $this->fNearRadius = $fRadius;
+        $this->sqlNear = 'ST_SetSRID(ST_Point('.$fLon.','.$fLat.'),4326)';
+    }
+
+    /**
+     * Check if the search is geographically restricted.
+     *
+     * Searches are restricted if a reference point is given or if
+     * a bounded viewbox is set.
+     *
+     * @return bool True, if the search is geographically bounded.
+     */
+    public function isBoundedSearch()
+    {
+        return $this->hasNearPoint() || ($this->sqlViewboxSmall && $this->bViewboxBounded);
+    }
+
+    /**
+     * Set rectangular viewbox.
+     *
+     * The viewbox may be bounded which means that no search results
+     * must be outside the viewbox.
+     *
+     * @param float[4] $aViewBox Coordinates of the viewbox.
+     * @param bool     $bBounded True if the viewbox is bounded.
+     *
+     * @return void
+     */
+    public function setViewboxFromBox(&$aViewBox, $bBounded)
+    {
+        $this->bViewboxBounded = $bBounded;
+        $this->sqlViewboxCentre = '';
+
+        $this->sqlViewboxSmall = sprintf(
+            'ST_SetSRID(ST_MakeBox2D(ST_Point(%F,%F),ST_Point(%F,%F)),4326)',
+            $aViewBox[0],
+            $aViewBox[1],
+            $aViewBox[2],
+            $aViewBox[3]
+        );
+
+        $fHeight = $aViewBox[0] - $aViewBox[2];
+        $fWidth = $aViewBox[1] - $aViewBox[3];
+
+        $this->sqlViewboxLarge = sprintf(
+            'ST_SetSRID(ST_MakeBox2D(ST_Point(%F,%F),ST_Point(%F,%F)),4326)',
+            max($aViewBox[0], $aViewBox[2]) + $fHeight,
+            max($aViewBox[1], $aViewBox[3]) + $fWidth,
+            min($aViewBox[0], $aViewBox[2]) - $fHeight,
+            min($aViewBox[1], $aViewBox[3]) - $fWidth
+        );
+    }
+
+    /**
+     * Set viewbox along a route.
+     *
+     * The viewbox may be bounded which means that no search results
+     * must be outside the viewbox.
+     *
+     * @param object   $oDB          DB connection to use for computing the box.
+     * @param string[] $aRoutePoints List of x,y coordinates along a route.
+     * @param float    $fRouteWidth  Buffer around the route to use.
+     * @param bool     $bBounded     True if the viewbox bounded.
+     *
+     * @return void
+     */
+    public function setViewboxFromRoute(&$oDB, $aRoutePoints, $fRouteWidth, $bBounded)
+    {
+        $this->bViewboxBounded = $bBounded;
+        $this->sqlViewboxCentre = "ST_SetSRID('LINESTRING(";
+        $sSep = '';
+        foreach ($aRoutePoints as $aPoint) {
+            $fPoint = (float)$aPoint;
+            $this->sqlViewboxCentre .= $sSep.$fPoint;
+            $sSep = ($sSep == ' ') ? ',' : ' ';
+        }
+        $this->sqlViewboxCentre .= ")'::geometry,4326)";
+
+        $sSQL = 'ST_BUFFER('.$this->sqlViewboxCentre.','.($fRouteWidth/69).')';
+        $sGeom = chksql($oDB->getOne("select ".$sSQL), "Could not get small viewbox");
+        $this->sqlViewboxSmall = "'".$sGeom."'::geometry";
+
+        $sSQL = 'ST_BUFFER('.$this->sqlViewboxCentre.','.($fRouteWidth/30).')';
+        $sGeom = chksql($oDB->getOne("select ".$sSQL), "Could not get large viewbox");
+        $this->sqlViewboxLarge = "'".$sGeom."'::geometry";
+    }
+
+    /**
+     * Set list of excluded place IDs.
+     *
+     * @param integer[] $aExcluded List of IDs.
+     *
+     * @return void
+     */
+    public function setExcludeList($aExcluded)
+    {
+        $this->sqlExcludeList = ' not in ('.join(',', $aExcluded).')';
+    }
+
+    /**
+     * Set list of countries to restrict search to.
+     *
+     * @param string[] $aCountries List of two-letter lower-case country codes.
+     *
+     * @return void
+     */
+    public function setCountryList($aCountries)
+    {
+        $this->sqlCountryList = '('.join(',', array_map('addQuotes', $aCountries)).')';
+    }
+
+    /**
+     * Extract a reference point from a query string.
+     *
+     * @param string $sQuery Query to scan.
+     *
+     * @return string The remaining query string.
+     */
+    public function setNearPointFromQuery($sQuery)
+    {
+        $aResult = parseLatLon($sQuery);
+
+        if ($aResult !== false
+            && $aResult[1] <= 90.1
+            && $aResult[1] >= -90.1
+            && $aResult[2] <= 180.1
+            && $aResult[2] >= -180.1
+        ) {
+            $this->setNearPoint($aResult[1], $aResult[2]);
+            $sQuery = trim(str_replace($aResult[0], ' ', $sQuery));
+        }
+
+        return $sQuery;
+    }
+
+    /**
+     * Get an SQL snipped for computing the distance from the reference point.
+     *
+     * @param string $sObj SQL variable name to compute the distance from.
+     *
+     * @return string An SQL string.
+     */
+    public function distanceSQL($sObj)
+    {
+        return 'ST_Distance('.$this->sqlNear.", $sObj)";
+    }
+
+    /**
+     * Get an SQL snipped for checking if something is within range of the
+     * reference point.
+     *
+     * @param string $sObj SQL variable name to compute if it is within range.
+     *
+     * @return string An SQL string.
+     */
+    public function withinSQL($sObj)
+    {
+        return sprintf('ST_DWithin(%s, %s, %F)', $sObj, $this->sqlNear, $this->fNearRadius);
+    }
+
+    /**
+     * Get an SQL snipped of the importance factor of the viewbox.
+     *
+     * The importance factor is computed by checking if an object is within
+     * the viewbox and/or the extended version of the viewbox.
+     *
+     * @param string $sObj SQL variable name of object to weight the importance
+     *
+     * @return string SQL snipped of the factor with a leading multiply sign.
+     */
+    public function viewboxImportanceSQL($sObj)
+    {
+        $sSQL = '';
+
+        if ($this->sqlViewboxSmall) {
+            $sSQL = " * CASE WHEN ST_Contains($this->sqlViewboxSmall, $sObj) THEN 1 ELSE 0.5 END";
+        }
+        if ($this->sqlViewboxLarge) {
+            $sSQL = " * CASE WHEN ST_Contains($this->sqlViewboxLarge, $sObj) THEN 1 ELSE 0.5 END";
+        }
+
+        return $sSQL;
+    }
+
+    /**
+     * SQL snipped checking if a place ID should be excluded.
+     *
+     * @param string $sVariable SQL variable name of place ID to check,
+     *                          potentially prefixed with more SQL.
+     *
+     * @return string SQL snippet.
+     */
+    public function excludeSQL($sVariable)
+    {
+        if ($this->sqlExcludeList) {
+            return $sVariable.$this->sqlExcludeList;
+        }
+
+        return '';
+    }
+}
diff --git a/lib/SearchDescription.php b/lib/SearchDescription.php
new file mode 100644 (file)
index 0000000..1f3765a
--- /dev/null
@@ -0,0 +1,1020 @@
+<?php
+
+namespace Nominatim;
+
+require_once(CONST_BasePath.'/lib/SpecialSearchOperator.php');
+require_once(CONST_BasePath.'/lib/SearchContext.php');
+
+/**
+ * Description of a single interpretation of a search query.
+ */
+class SearchDescription
+{
+    /// Ranking how well the description fits the query.
+    private $iSearchRank = 0;
+    /// Country code of country the result must belong to.
+    private $sCountryCode = '';
+    /// List of word ids making up the name of the object.
+    private $aName = array();
+    /// List of word ids making up the address of the object.
+    private $aAddress = array();
+    /// Subset of word ids of full words making up the address.
+    private $aFullNameAddress = array();
+    /// List of word ids that appear in the name but should be ignored.
+    private $aNameNonSearch = array();
+    /// List of word ids that appear in the address but should be ignored.
+    private $aAddressNonSearch = array();
+    /// Kind of search for special searches, see Nominatim::Operator.
+    private $iOperator = Operator::NONE;
+    /// Class of special feature to search for.
+    private $sClass = '';
+    /// Type of special feature to search for.
+    private $sType = '';
+    /// Housenumber of the object.
+    private $sHouseNumber = '';
+    /// Postcode for the object.
+    private $sPostcode = '';
+    /// Global search constraints.
+    private $oContext;
+
+    // Temporary values used while creating the search description.
+
+    /// Index of phrase currently processed.
+    private $iNamePhrase = -1;
+
+
+    /**
+     * Create an empty search description.
+     *
+     * @param object $oContext Global context to use. Will be inherited by
+     *                         all derived search objects.
+     */
+    public function __construct($oContext)
+    {
+        $this->oContext = $oContext;
+    }
+
+    /**
+     * Get current search rank.
+     *
+     * The higher the search rank the lower the likelyhood that the
+     * search is a correct interpretation of the search query.
+     *
+     * @return integer Search rank.
+     */
+    public function getRank()
+    {
+        return $this->iSearchRank;
+    }
+
+    /**
+     * Increase the search rank.
+     *
+     * @param integer $iAddRank Number of ranks to increase.
+     *
+     * @return void
+     */
+    public function addToRank($iAddRank)
+    {
+        $this->iSearchRank += $iAddRank;
+        return $this->iSearchRank;
+    }
+
+    /**
+     * Make this search a POI search.
+     *
+     * In a POI search, objects are not (only) searched by their name
+     * but also by the primary OSM key/value pair (class and type in Nominatim).
+     *
+     * @param integer $iOperator Type of POI search
+     * @param string  $sClass    Class (or OSM tag key) of POI.
+     * @param string  $sType     Type (or OSM tag value) of POI.
+     *
+     * @return void
+     */
+    public function setPoiSearch($iOperator, $sClass, $sType)
+    {
+        $this->iOperator = $iOperator;
+        $this->sClass = $sClass;
+        $this->sType = $sType;
+    }
+
+    /**
+     * Check if this might be a full address search.
+     *
+     * @return bool True if the search contains name, address and housenumber.
+     */
+    public function looksLikeFullAddress()
+    {
+        return sizeof($this->aName)
+               && (sizeof($this->aAddress || $this->sCountryCode))
+               && preg_match('/[0-9]+/', $this->sHouseNumber);
+    }
+
+    /**
+     * Check if any operator is set.
+     *
+     * @return bool True, if this is a special search operation.
+     */
+    public function hasOperator()
+    {
+        return $this->iOperator != Operator::NONE;
+    }
+
+    /**
+     * Extract key/value pairs from a query.
+     *
+     * Key/value pairs are recognised if they are of the form [<key>=<value>].
+     * If multiple terms of this kind are found then all terms are removed
+     * but only the first is used for search.
+     *
+     * @param string $sQuery Original query string.
+     *
+     * @return string The query string with the special search patterns removed.
+     */
+    public function extractKeyValuePairs($sQuery)
+    {
+        // Search for terms of kind [<key>=<value>].
+        preg_match_all(
+            '/\\[([\\w_]*)=([\\w_]*)\\]/',
+            $sQuery,
+            $aSpecialTermsRaw,
+            PREG_SET_ORDER
+        );
+
+        foreach ($aSpecialTermsRaw as $aTerm) {
+            $sQuery = str_replace($aTerm[0], ' ', $sQuery);
+            if (!$this->hasOperator()) {
+                $this->setPoiSearch(Operator::TYPE, $aTerm[1], $aTerm[2]);
+            }
+        }
+
+        return $sQuery;
+    }
+
+    /**
+     * Check if the combination of parameters is sensible.
+     *
+     * @param string[] $aCountryCodes List of country codes.
+     *
+     * @return bool True, if the search looks valid.
+     */
+    public function isValidSearch(&$aCountryCodes)
+    {
+        if (!sizeof($this->aName)) {
+            if ($this->sHouseNumber) {
+                return false;
+            }
+        }
+        if ($aCountryCodes
+            && $this->sCountryCode
+            && !in_array($this->sCountryCode, $aCountryCodes)
+        ) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /////////// Search building functions
+
+
+    /**
+     * Derive new searches by adding a full term to the existing search.
+     *
+     * @param mixed[] $aSearchTerm  Description of the token.
+     * @param bool    $bWordInQuery True, if the normalised version of the word
+     *                              is contained in the query.
+     * @param bool    $bHasPartial  True if there are also tokens of partial terms
+     *                              with the same name.
+     * @param string  $sPhraseType  Type of phrase the token is contained in.
+     * @param bool    $bFirstToken  True if the token is at the beginning of the
+     *                              query.
+     * @param bool    $bFirstPhrase True if the token is in the first phrase of
+     *                              the query.
+     * @param bool    $bLastToken   True if the token is at the end of the query.
+     * @param integer $iGlobalRank  Changable ranking of all searches in the
+     *                              batch.
+     *
+     * @return SearchDescription[] List of derived search descriptions.
+     */
+    public function extendWithFullTerm($aSearchTerm, $bWordInQuery, $bHasPartial, $sPhraseType, $bFirstToken, $bFirstPhrase, $bLastToken, &$iGlobalRank)
+    {
+        $aNewSearches = array();
+
+        if (($sPhraseType == '' || $sPhraseType == 'country')
+            && !empty($aSearchTerm['country_code'])
+            && $aSearchTerm['country_code'] != '0'
+        ) {
+            if (!$this->sCountryCode) {
+                $oSearch = clone $this;
+                $oSearch->iSearchRank++;
+                $oSearch->sCountryCode = $aSearchTerm['country_code'];
+                // Country is almost always at the end of the string
+                // - increase score for finding it anywhere else (optimisation)
+                if (!$bLastToken) {
+                    $oSearch->iSearchRank += 5;
+                }
+                $aNewSearches[] = $oSearch;
+
+                // If it is at the beginning, we can be almost sure that
+                // the terms are in the wrong order. Increase score for all searches.
+                if ($bFirstToken) {
+                    $iGlobalRank++;
+                }
+            }
+        } elseif (($sPhraseType == '' || $sPhraseType == 'postalcode')
+                  && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'postcode'
+        ) {
+            // We need to try the case where the postal code is the primary element
+            // (i.e. no way to tell if it is (postalcode, city) OR (city, postalcode)
+            // so try both.
+            if (!$this->sPostcode && $bWordInQuery
+                && pg_escape_string($aSearchTerm['word']) == $aSearchTerm['word']
+            ) {
+                // If we have structured search or this is the first term,
+                // make the postcode the primary search element.
+                if ($this->iOperator == Operator::NONE
+                    && ($sPhraseType == 'postalcode' || $bFirstToken)
+                ) {
+                    $oSearch = clone $this;
+                    $oSearch->iSearchRank++;
+                    $oSearch->iOperator = Operator::POSTCODE;
+                    $oSearch->aAddress = array_merge($this->aAddress, $this->aName);
+                    $oSearch->aName =
+                        array($aSearchTerm['word_id'] => $aSearchTerm['word']);
+                    $aNewSearches[] = $oSearch;
+                }
+
+                // If we have a structured search or this is not the first term,
+                // add the postcode as an addendum.
+                if ($this->iOperator != Operator::POSTCODE
+                    && ($sPhraseType == 'postalcode' || sizeof($this->aName))
+                ) {
+                    $oSearch = clone $this;
+                    $oSearch->iSearchRank++;
+                    $oSearch->sPostcode = $aSearchTerm['word'];
+                    $aNewSearches[] = $oSearch;
+                }
+            }
+        } elseif (($sPhraseType == '' || $sPhraseType == 'street')
+                 && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house'
+        ) {
+            if (!$this->sHouseNumber && $this->iOperator != Operator::POSTCODE) {
+                $oSearch = clone $this;
+                $oSearch->iSearchRank++;
+                $oSearch->sHouseNumber = trim($aSearchTerm['word_token']);
+                // sanity check: if the housenumber is not mainly made
+                // up of numbers, add a penalty
+                if (preg_match_all("/[^0-9]/", $oSearch->sHouseNumber, $aMatches) > 2) {
+                    $oSearch->iSearchRank++;
+                }
+                if (!isset($aSearchTerm['word_id'])) {
+                    $oSearch->iSearchRank++;
+                }
+                // also must not appear in the middle of the address
+                if (sizeof($this->aAddress) || sizeof($this->aAddressNonSearch)) {
+                    $oSearch->iSearchRank++;
+                }
+                $aNewSearches[] = $oSearch;
+            }
+        } elseif ($sPhraseType == ''
+                  && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null
+        ) {
+            // require a normalized exact match of the term
+            // if we have the normalizer version of the query
+            // available
+            if ($this->iOperator == Operator::NONE
+                && (isset($aSearchTerm['word']) && $aSearchTerm['word'])
+                && $bWordInQuery
+            ) {
+                $oSearch = clone $this;
+                $oSearch->iSearchRank++;
+
+                $iOp = Operator::NEAR; // near == in for the moment
+                if ($aSearchTerm['operator'] == '') {
+                    if (sizeof($this->aName)) {
+                        $iOp = Operator::NAME;
+                    }
+                    $oSearch->iSearchRank += 2;
+                }
+
+                $oSearch->setPoiSearch($iOp, $aSearchTerm['class'], $aSearchTerm['type']);
+                $aNewSearches[] = $oSearch;
+            }
+        } elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id']) {
+            $iWordID = $aSearchTerm['word_id'];
+            if (sizeof($this->aName)) {
+                if (($sPhraseType == '' || !$bFirstPhrase)
+                    && $sPhraseType != 'country'
+                    && !$bHasPartial
+                ) {
+                    $oSearch = clone $this;
+                    $oSearch->iSearchRank++;
+                    $oSearch->aAddress[$iWordID] = $iWordID;
+                    $aNewSearches[] = $oSearch;
+                } else {
+                    $this->aFullNameAddress[$iWordID] = $iWordID;
+                }
+            } else {
+                $oSearch = clone $this;
+                $oSearch->iSearchRank++;
+                $oSearch->aName = array($iWordID => $iWordID);
+                $aNewSearches[] = $oSearch;
+            }
+        }
+
+        return $aNewSearches;
+    }
+
+    /**
+     * Derive new searches by adding a partial term to the existing search.
+     *
+     * @param mixed[] $aSearchTerm          Description of the token.
+     * @param bool    $bStructuredPhrases   True if the search is structured.
+     * @param integer $iPhrase              Number of the phrase the token is in.
+     * @param mixed[] $aWordFrequencyScores Number of times tokens appears
+     *                                      overall in a planet database.
+     * @param array[] $aFullTokens          List of full term tokens with the
+     *                                      same name.
+     *
+     * @return SearchDescription[] List of derived search descriptions.
+     */
+    public function extendWithPartialTerm($aSearchTerm, $bStructuredPhrases, $iPhrase, &$aWordFrequencyScores, $aFullTokens)
+    {
+        // Only allow name terms.
+        if (!(isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])) {
+            return array();
+        }
+
+        $aNewSearches = array();
+        $iWordID = $aSearchTerm['word_id'];
+
+        if ((!$bStructuredPhrases || $iPhrase > 0)
+            && sizeof($this->aName)
+            && strpos($aSearchTerm['word_token'], ' ') === false
+        ) {
+            if ($aWordFrequencyScores[$iWordID] < CONST_Max_Word_Frequency) {
+                $oSearch = clone $this;
+                $oSearch->iSearchRank++;
+                $oSearch->aAddress[$iWordID] = $iWordID;
+                $aNewSearches[] = $oSearch;
+            } else {
+                $oSearch = clone $this;
+                $oSearch->iSearchRank++;
+                $oSearch->aAddressNonSearch[$iWordID] = $iWordID;
+                if (preg_match('#^[0-9]+$#', $aSearchTerm['word_token'])) {
+                    $oSearch->iSearchRank += 2;
+                }
+                if (sizeof($aFullTokens)) {
+                    $oSearch->iSearchRank++;
+                }
+                $aNewSearches[] = $oSearch;
+
+                // revert to the token version?
+                foreach ($aFullTokens as $aSearchTermToken) {
+                    if (empty($aSearchTermToken['country_code'])
+                        && empty($aSearchTermToken['lat'])
+                        && empty($aSearchTermToken['class'])
+                    ) {
+                        $oSearch = clone $this;
+                        $oSearch->iSearchRank++;
+                        $oSearch->aAddress[$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
+                        $aNewSearches[] = $oSearch;
+                    }
+                }
+            }
+        }
+
+        if ((!$this->sPostcode && !$this->aAddress && !$this->aAddressNonSearch)
+            && (!sizeof($this->aName) || $this->iNamePhrase == $iPhrase)
+        ) {
+            $oSearch = clone $this;
+            $oSearch->iSearchRank++;
+            if (!sizeof($this->aName)) {
+                $oSearch->iSearchRank += 1;
+            }
+            if (preg_match('#^[0-9]+$#', $aSearchTerm['word_token'])) {
+                $oSearch->iSearchRank += 2;
+            }
+            if ($aWordFrequencyScores[$iWordID] < CONST_Max_Word_Frequency) {
+                $oSearch->aName[$iWordID] = $iWordID;
+            } else {
+                $oSearch->aNameNonSearch[$iWordID] = $iWordID;
+            }
+            $oSearch->iNamePhrase = $iPhrase;
+            $aNewSearches[] = $oSearch;
+        }
+
+        return $aNewSearches;
+    }
+
+    /////////// Query functions
+
+
+    /**
+     * Query database for places that match this search.
+     *
+     * @param object  $oDB                  Database connection to use.
+     * @param mixed[] $aWordFrequencyScores Number of times tokens appears
+     *                                      overall in a planet database.
+     * @param mixed[] $aExactMatchCache     Saves number of exact matches.
+     * @param integer $iMinRank             Minimum address rank to restrict
+     *                                      search to.
+     * @param integer $iMaxRank             Maximum address rank to restrict
+     *                                      search to.
+     * @param integer $iLimit               Maximum number of results.
+     *
+     * @return mixed[] An array with two fields: IDs contains the list of
+     *                 matching place IDs and houseNumber the houseNumber
+     *                 if appicable or -1 if not.
+     */
+    public function query(&$oDB, &$aWordFrequencyScores, &$aExactMatchCache, $iMinRank, $iMaxRank, $iLimit)
+    {
+        $aPlaceIDs = array();
+        $iHousenumber = -1;
+
+        if ($this->sCountryCode
+            && !sizeof($this->aName)
+            && !$this->iOperator
+            && !$this->sClass
+            && !$this->oContext->hasNearPoint()
+        ) {
+            // Just looking for a country - look it up
+            if (4 >= $iMinRank && 4 <= $iMaxRank) {
+                $aPlaceIDs = $this->queryCountry($oDB);
+            }
+        } elseif (!sizeof($this->aName) && !sizeof($this->aAddress)) {
+            // Neither name nor address? Then we must be
+            // looking for a POI in a geographic area.
+            if ($this->oContext->isBoundedSearch()) {
+                $aPlaceIDs = $this->queryNearbyPoi($oDB, $iLimit);
+            }
+        } elseif ($this->iOperator == Operator::POSTCODE) {
+            // looking for postcode
+            $aPlaceIDs = $this->queryPostcode($oDB, $iLimit);
+        } else {
+            // Ordinary search:
+            // First search for places according to name and address.
+            $aNamedPlaceIDs = $this->queryNamedPlace(
+                $oDB,
+                $aWordFrequencyScores,
+                $iMinRank,
+                $iMaxRank,
+                $iLimit
+            );
+
+            if (sizeof($aNamedPlaceIDs)) {
+                foreach ($aNamedPlaceIDs as $aRow) {
+                    $aPlaceIDs[] = $aRow['place_id'];
+                    $aExactMatchCache[$aRow['place_id']] = $aRow['exactmatch'];
+                }
+            }
+
+            //now search for housenumber, if housenumber provided
+            if ($this->sHouseNumber && sizeof($aPlaceIDs)) {
+                $aResult = $this->queryHouseNumber($oDB, $aPlaceIDs, $iLimit);
+
+                if (sizeof($aResult)) {
+                    $iHousenumber = $aResult['iHouseNumber'];
+                    $aPlaceIDs = $aResult['aPlaceIDs'];
+                } elseif (!$this->looksLikeFullAddress()) {
+                    $aPlaceIDs = array();
+                }
+            }
+
+            // finally get POIs if requested
+            if ($this->sClass && sizeof($aPlaceIDs)) {
+                $aPlaceIDs = $this->queryPoiByOperator($oDB, $aPlaceIDs, $iLimit);
+            }
+        }
+
+        if (CONST_Debug) {
+            echo "<br><b>Place IDs:</b> ";
+            var_Dump($aPlaceIDs);
+        }
+
+        if (sizeof($aPlaceIDs) && $this->sPostcode) {
+            $sSQL = 'SELECT place_id FROM placex';
+            $sSQL .= ' WHERE place_id in ('.join(',', $aPlaceIDs).')';
+            $sSQL .= " AND postcode = '".$this->sPostcode."'";
+            if (CONST_Debug) var_dump($sSQL);
+            $aFilteredPlaceIDs = chksql($oDB->getCol($sSQL));
+            if ($aFilteredPlaceIDs) {
+                $aPlaceIDs = $aFilteredPlaceIDs;
+                if (CONST_Debug) {
+                    echo "<br><b>Place IDs after postcode filtering:</b> ";
+                    var_Dump($aPlaceIDs);
+                }
+            }
+        }
+
+        return array('IDs' => $aPlaceIDs, 'houseNumber' => $iHousenumber);
+    }
+
+
+    private function queryCountry(&$oDB)
+    {
+        $sSQL = 'SELECT place_id FROM placex ';
+        $sSQL .= "WHERE country_code='".$this->sCountryCode."'";
+        $sSQL .= ' AND rank_search = 4';
+        if ($this->oContext->bViewboxBounded) {
+            $sSQL .= ' AND ST_Intersects('.$this->oContext->sqlViewboxSmall.', geometry)';
+        }
+        $sSQL .= " ORDER BY st_area(geometry) DESC LIMIT 1";
+
+        if (CONST_Debug) var_dump($sSQL);
+
+        return chksql($oDB->getCol($sSQL));
+    }
+
+    private function queryNearbyPoi(&$oDB, $iLimit)
+    {
+        if (!$this->sClass) {
+            return array();
+        }
+
+        $sPoiTable = $this->poiTable();
+
+        $sSQL = 'SELECT count(*) FROM pg_tables WHERE tablename = \''.$sPoiTable."'";
+        if (chksql($oDB->getOne($sSQL))) {
+            $sSQL = 'SELECT place_id FROM '.$sPoiTable.' ct';
+            if ($this->oContext->sqlCountryList) {
+                $sSQL .= ' JOIN placex USING (place_id)';
+            }
+            if ($this->oContext->hasNearPoint()) {
+                $sSQL .= ' WHERE '.$this->oContext->withinSQL('ct.centroid');
+            } elseif ($this->oContext->bViewboxBounded) {
+                $sSQL .= ' WHERE ST_Contains('.$this->oContext->sqlViewboxSmall.', ct.centroid)';
+            }
+            if ($this->oContext->sqlCountryList) {
+                $sSQL .= ' AND country_code in '.$this->oContext->sqlCountryList;
+            }
+            $sSQL .= $this->oContext->excludeSQL(' AND place_id');
+            if ($this->oContext->sqlViewboxCentre) {
+                $sSQL .= ' ORDER BY ST_Distance(';
+                $sSQL .= $this->oContext->sqlViewboxCentre.', ct.centroid) ASC';
+            } elseif ($this->oContext->hasNearPoint()) {
+                $sSQL .= ' ORDER BY '.$this->oContext->distanceSQL('ct.centroid').' ASC';
+            }
+            $sSQL .= " limit $iLimit";
+            if (CONST_Debug) var_dump($sSQL);
+            return chksql($oDB->getCol($sSQL));
+        }
+
+        if ($this->oContext->hasNearPoint()) {
+            $sSQL = 'SELECT place_id FROM placex WHERE ';
+            $sSQL .= 'class=\''.$this->sClass."' and type='".$this->sType."'";
+            $sSQL .= ' AND '.$this->oContext->withinSQL('geometry');
+            $sSQL .= ' AND linked_place_id is null';
+            if ($this->oContext->sqlCountryList) {
+                $sSQL .= ' AND country_code in '.$this->oContext->sqlCountryList;
+            }
+            $sSQL .= ' ORDER BY '.$this->oContext->distanceSQL('centroid')." ASC";
+            $sSQL .= " LIMIT $iLimit";
+            if (CONST_Debug) var_dump($sSQL);
+            return chksql($oDB->getCol($sSQL));
+        }
+
+        return array();
+    }
+
+    private function queryPostcode(&$oDB, $iLimit)
+    {
+        $sSQL = 'SELECT p.place_id FROM location_postcode p ';
+
+        if (sizeof($this->aAddress)) {
+            $sSQL .= ', search_name s ';
+            $sSQL .= 'WHERE s.place_id = p.parent_place_id ';
+            $sSQL .= 'AND array_cat(s.nameaddress_vector, s.name_vector)';
+            $sSQL .= '      @> '.getArraySQL($this->aAddress).' AND ';
+        } else {
+            $sSQL .= 'WHERE ';
+        }
+
+        $sSQL .= "p.postcode = '".reset($this->aName)."'";
+        $sSQL .= $this->countryCodeSQL(' AND p.country_code');
+        $sSQL .= $this->oContext->excludeSQL(' AND p.place_id');
+        $sSQL .= " LIMIT $iLimit";
+
+        if (CONST_Debug) var_dump($sSQL);
+
+        return chksql($oDB->getCol($sSQL));
+    }
+
+    private function queryNamedPlace(&$oDB, $aWordFrequencyScores, $iMinAddressRank, $iMaxAddressRank, $iLimit)
+    {
+        $aTerms = array();
+        $aOrder = array();
+
+        if ($this->sHouseNumber && sizeof($this->aAddress)) {
+            $sHouseNumberRegex = '\\\\m'.$this->sHouseNumber.'\\\\M';
+            $aOrder[] = ' (';
+            $aOrder[0] .= 'EXISTS(';
+            $aOrder[0] .= '  SELECT place_id';
+            $aOrder[0] .= '  FROM placex';
+            $aOrder[0] .= '  WHERE parent_place_id = search_name.place_id';
+            $aOrder[0] .= "    AND transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
+            $aOrder[0] .= '  LIMIT 1';
+            $aOrder[0] .= ') ';
+            // also housenumbers from interpolation lines table are needed
+            if (preg_match('/[0-9]+/', $this->sHouseNumber)) {
+                $iHouseNumber = intval($this->sHouseNumber);
+                $aOrder[0] .= 'OR EXISTS(';
+                $aOrder[0] .= '  SELECT place_id ';
+                $aOrder[0] .= '  FROM location_property_osmline ';
+                $aOrder[0] .= '  WHERE parent_place_id = search_name.place_id';
+                $aOrder[0] .= '    AND startnumber is not NULL';
+                $aOrder[0] .= '    AND '.$iHouseNumber.'>=startnumber ';
+                $aOrder[0] .= '    AND '.$iHouseNumber.'<=endnumber ';
+                $aOrder[0] .= '  LIMIT 1';
+                $aOrder[0] .= ')';
+            }
+            $aOrder[0] .= ') DESC';
+        }
+
+        if (sizeof($this->aName)) {
+            $aTerms[] = 'name_vector @> '.getArraySQL($this->aName);
+        }
+        if (sizeof($this->aAddress)) {
+            // For infrequent name terms disable index usage for address
+            if (CONST_Search_NameOnlySearchFrequencyThreshold
+                && sizeof($this->aName) == 1
+                && $aWordFrequencyScores[$this->aName[reset($this->aName)]]
+                     < CONST_Search_NameOnlySearchFrequencyThreshold
+            ) {
+                $aTerms[] = 'array_cat(nameaddress_vector,ARRAY[]::integer[]) @> '.getArraySQL($this->aAddress);
+            } else {
+                $aTerms[] = 'nameaddress_vector @> '.getArraySQL($this->aAddress);
+            }
+        }
+
+        $sCountryTerm = $this->countryCodeSQL('country_code');
+        if ($sCountryTerm) {
+            $aTerms[] = $sCountryTerm;
+        }
+
+        if ($this->sHouseNumber) {
+            $aTerms[] = "address_rank between 16 and 27";
+        } elseif (!$this->sClass || $this->iOperator == Operator::NAME) {
+            if ($iMinAddressRank > 0) {
+                $aTerms[] = "address_rank >= ".$iMinAddressRank;
+            }
+            if ($iMaxAddressRank < 30) {
+                $aTerms[] = "address_rank <= ".$iMaxAddressRank;
+            }
+        }
+
+        if ($this->oContext->hasNearPoint()) {
+            $aTerms[] = $this->oContext->withinSQL('centroid');
+            $aOrder[] = $this->oContext->distanceSQL('centroid');
+        } elseif ($this->sPostcode) {
+            if (!sizeof($this->aAddress)) {
+                $aTerms[] = "EXISTS(SELECT place_id FROM location_postcode p WHERE p.postcode = '".$this->sPostcode."' AND ST_DWithin(search_name.centroid, p.geometry, 0.1))";
+            } else {
+                $aOrder[] = "(SELECT min(ST_Distance(search_name.centroid, p.geometry)) FROM location_postcode p WHERE p.postcode = '".$this->sPostcode."')";
+            }
+        }
+
+        $sExcludeSQL = $this->oContext->excludeSQL('place_id');
+        if ($sExcludeSQL) {
+            $aTerms[] = $sExcludeSQL;
+        }
+
+        if ($this->oContext->bViewboxBounded) {
+            $aTerms[] = 'centroid && '.$this->oContext->sqlViewboxSmall;
+        }
+
+        if ($this->oContext->hasNearPoint()) {
+            $aOrder[] = $this->oContext->distanceSQL('centroid');
+        }
+
+        if ($this->sHouseNumber) {
+            $sImportanceSQL = '- abs(26 - address_rank) + 3';
+        } else {
+            $sImportanceSQL = '(CASE WHEN importance = 0 OR importance IS NULL THEN 0.75-(search_rank::float/40) ELSE importance END)';
+        }
+        $sImportanceSQL .= $this->oContext->viewboxImportanceSQL('centroid');
+        $aOrder[] = "$sImportanceSQL DESC";
+
+        if (sizeof($this->aFullNameAddress)) {
+            $sExactMatchSQL = ' ( ';
+            $sExactMatchSQL .= ' SELECT count(*) FROM ( ';
+            $sExactMatchSQL .= '  SELECT unnest('.getArraySQL($this->aFullNameAddress).')';
+            $sExactMatchSQL .= '    INTERSECT ';
+            $sExactMatchSQL .= '  SELECT unnest(nameaddress_vector)';
+            $sExactMatchSQL .= ' ) s';
+            $sExactMatchSQL .= ') as exactmatch';
+            $aOrder[] = 'exactmatch DESC';
+        } else {
+            $sExactMatchSQL = '0::int as exactmatch';
+        }
+
+        if ($this->sHouseNumber || $this->sClass) {
+            $iLimit = 20;
+        }
+
+        if (sizeof($aTerms)) {
+            $sSQL = 'SELECT place_id,'.$sExactMatchSQL;
+            $sSQL .= ' FROM search_name';
+            $sSQL .= ' WHERE '.join(' and ', $aTerms);
+            $sSQL .= ' ORDER BY '.join(', ', $aOrder);
+            $sSQL .= ' LIMIT '.$iLimit;
+
+            if (CONST_Debug) var_dump($sSQL);
+
+            return chksql(
+                $oDB->getAll($sSQL),
+                "Could not get places for search terms."
+            );
+        }
+
+        return array();
+    }
+
+    private function queryHouseNumber(&$oDB, $aRoadPlaceIDs, $iLimit)
+    {
+        $sPlaceIDs = join(',', $aRoadPlaceIDs);
+
+        $sHouseNumberRegex = '\\\\m'.$this->sHouseNumber.'\\\\M';
+        $sSQL = 'SELECT place_id FROM placex ';
+        $sSQL .= 'WHERE parent_place_id in ('.$sPlaceIDs.')';
+        $sSQL .= "  AND transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
+        $sSQL .= $this->oContext->excludeSQL(' AND place_id');
+        $sSQL .= " LIMIT $iLimit";
+
+        if (CONST_Debug) var_dump($sSQL);
+
+        $aPlaceIDs = chksql($oDB->getCol($sSQL));
+
+        if (sizeof($aPlaceIDs)) {
+            return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => -1);
+        }
+
+        $bIsIntHouseNumber= (bool) preg_match('/[0-9]+/', $this->sHouseNumber);
+        $iHousenumber = intval($this->sHouseNumber);
+        if ($bIsIntHouseNumber) {
+            // if nothing found, search in the interpolation line table
+            $sSQL = 'SELECT distinct place_id FROM location_property_osmline';
+            $sSQL .= ' WHERE startnumber is not NULL';
+            $sSQL .= '  AND parent_place_id in ('.$sPlaceIDs.') AND (';
+            if ($iHousenumber % 2 == 0) {
+                // If housenumber is even, look for housenumber in streets
+                // with interpolationtype even or all.
+                $sSQL .= "interpolationtype='even'";
+            } else {
+                // Else look for housenumber with interpolationtype odd or all.
+                $sSQL .= "interpolationtype='odd'";
+            }
+            $sSQL .= " or interpolationtype='all') and ";
+            $sSQL .= $iHousenumber.">=startnumber and ";
+            $sSQL .= $iHousenumber."<=endnumber";
+            $sSQL .= $this->oContext->excludeSQL(' AND place_id');
+            $sSQL .= " limit $iLimit";
+
+            if (CONST_Debug) var_dump($sSQL);
+
+            $aPlaceIDs = chksql($oDB->getCol($sSQL, 0));
+
+            if (sizeof($aPlaceIDs)) {
+                return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => $iHousenumber);
+            }
+        }
+
+        // If nothing found try the aux fallback table
+        if (CONST_Use_Aux_Location_data) {
+            $sSQL = 'SELECT place_id FROM location_property_aux';
+            $sSQL .= ' WHERE parent_place_id in ('.$sPlaceIDs.')';
+            $sSQL .= " AND housenumber = '".$this->sHouseNumber."'";
+            $sSQL .= $this->oContext->excludeSQL(' AND place_id');
+            $sSQL .= " limit $iLimit";
+
+            if (CONST_Debug) var_dump($sSQL);
+
+            $aPlaceIDs = chksql($oDB->getCol($sSQL));
+
+            if (sizeof($aPlaceIDs)) {
+                return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => -1);
+            }
+        }
+
+        // If nothing found then search in Tiger data (location_property_tiger)
+        if (CONST_Use_US_Tiger_Data && $bIsIntHouseNumber) {
+            $sSQL = 'SELECT distinct place_id FROM location_property_tiger';
+            $sSQL .= ' WHERE parent_place_id in ('.$sPlaceIDs.') and (';
+            if ($iHousenumber % 2 == 0) {
+                $sSQL .= "interpolationtype='even'";
+            } else {
+                $sSQL .= "interpolationtype='odd'";
+            }
+            $sSQL .= " or interpolationtype='all') and ";
+            $sSQL .= $iHousenumber.">=startnumber and ";
+            $sSQL .= $iHousenumber."<=endnumber";
+            $sSQL .= $this->oContext->excludeSQL(' AND place_id');
+            $sSQL .= " limit $iLimit";
+
+            if (CONST_Debug) var_dump($sSQL);
+
+            $aPlaceIDs = chksql($oDB->getCol($sSQL, 0));
+
+            if (sizeof($aPlaceIDs)) {
+                return array('aPlaceIDs' => $aPlaceIDs, 'iHouseNumber' => $iHousenumber);
+            }
+        }
+
+        return array();
+    }
+
+
+    private function queryPoiByOperator(&$oDB, $aParentIDs, $iLimit)
+    {
+        $sPlaceIDs = join(',', $aParentIDs);
+        $aClassPlaceIDs = array();
+
+        if ($this->iOperator == Operator::TYPE || $this->iOperator == Operator::NAME) {
+            // If they were searching for a named class (i.e. 'Kings Head pub')
+            // then we might have an extra match
+            $sSQL = 'SELECT place_id FROM placex ';
+            $sSQL .= " WHERE place_id in ($sPlaceIDs)";
+            $sSQL .= "   AND class='".$this->sClass."' ";
+            $sSQL .= "   AND type='".$this->sType."'";
+            $sSQL .= "   AND linked_place_id is null";
+            $sSQL .= $this->oContext->excludeSQL(' AND place_id');
+            $sSQL .= " ORDER BY rank_search ASC ";
+            $sSQL .= " LIMIT $iLimit";
+
+            if (CONST_Debug) var_dump($sSQL);
+
+            $aClassPlaceIDs = chksql($oDB->getCol($sSQL));
+        }
+
+        // NEAR and IN are handled the same
+        if ($this->iOperator == Operator::TYPE || $this->iOperator == Operator::NEAR) {
+            $sClassTable = $this->poiTable();
+            $sSQL = "SELECT count(*) FROM pg_tables WHERE tablename = '$sClassTable'";
+            $bCacheTable = (bool) chksql($oDB->getOne($sSQL));
+
+            $sSQL = "SELECT min(rank_search) FROM placex WHERE place_id in ($sPlaceIDs)";
+            if (CONST_Debug) var_dump($sSQL);
+            $iMaxRank = (int)chksql($oDB->getOne($sSQL));
+
+            // For state / country level searches the normal radius search doesn't work very well
+            $sPlaceGeom = false;
+            if ($iMaxRank < 9 && $bCacheTable) {
+                // Try and get a polygon to search in instead
+                $sSQL = 'SELECT geometry FROM placex';
+                $sSQL .= " WHERE place_id in ($sPlaceIDs)";
+                $sSQL .= "   AND rank_search < $iMaxRank + 5";
+                $sSQL .= "   AND ST_GeometryType(geometry) in ('ST_Polygon','ST_MultiPolygon')";
+                $sSQL .= " ORDER BY rank_search ASC ";
+                $sSQL .= " LIMIT 1";
+                if (CONST_Debug) var_dump($sSQL);
+                $sPlaceGeom = chksql($oDB->getOne($sSQL));
+            }
+
+            if ($sPlaceGeom) {
+                $sPlaceIDs = false;
+            } else {
+                $iMaxRank += 5;
+                $sSQL = 'SELECT place_id FROM placex';
+                $sSQL .= " WHERE place_id in ($sPlaceIDs) and rank_search < $iMaxRank";
+                if (CONST_Debug) var_dump($sSQL);
+                $aPlaceIDs = chksql($oDB->getCol($sSQL));
+                $sPlaceIDs = join(',', $aPlaceIDs);
+            }
+
+            if ($sPlaceIDs || $sPlaceGeom) {
+                $fRange = 0.01;
+                if ($bCacheTable) {
+                    // More efficient - can make the range bigger
+                    $fRange = 0.05;
+
+                    $sOrderBySQL = '';
+                    if ($this->oContext->hasNearPoint()) {
+                        $sOrderBySQL = $this->oContext->distanceSQL('l.centroid');
+                    } elseif ($sPlaceIDs) {
+                        $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
+                    } elseif ($sPlaceGeom) {
+                        $sOrderBySQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
+                    }
+
+                    $sSQL = 'SELECT distinct i.place_id';
+                    if ($sOrderBySQL) {
+                        $sSQL .= ', i.order_term';
+                    }
+                    $sSQL .= ' from (SELECT l.place_id';
+                    if ($sOrderBySQL) {
+                        $sSQL .= ','.$sOrderBySQL.' as order_term';
+                    }
+                    $sSQL .= ' from '.$sClassTable.' as l';
+
+                    if ($sPlaceIDs) {
+                        $sSQL .= ",placex as f WHERE ";
+                        $sSQL .= "f.place_id in ($sPlaceIDs) ";
+                        $sSQL .= " AND ST_DWithin(l.centroid, f.centroid, $fRange)";
+                    } elseif ($sPlaceGeom) {
+                        $sSQL .= " WHERE ST_Contains('$sPlaceGeom', l.centroid)";
+                    }
+
+                    $sSQL .= $this->oContext->excludeSQL(' AND l.place_id');
+                    $sSQL .= 'limit 300) i ';
+                    if ($sOrderBySQL) {
+                        $sSQL .= 'order by order_term asc';
+                    }
+                    $sSQL .= " limit $iLimit";
+
+                    if (CONST_Debug) var_dump($sSQL);
+
+                    $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($oDB->getCol($sSQL)));
+                } else {
+                    if ($this->oContext->hasNearPoint()) {
+                        $fRange = $this->oContext->nearRadius();
+                    }
+
+                    $sOrderBySQL = '';
+                    if ($this->oContext->hasNearPoint()) {
+                        $sOrderBySQL = $this->oContext->distanceSQL('l.geometry');
+                    } else {
+                        $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
+                    }
+
+                    $sSQL = 'SELECT distinct l.place_id';
+                    if ($sOrderBySQL) {
+                        $sSQL .= ','.$sOrderBySQL.' as orderterm';
+                    }
+                    $sSQL .= ' FROM placex as l, placex as f';
+                    $sSQL .= " WHERE f.place_id in ($sPlaceIDs)";
+                    $sSQL .= "  AND ST_DWithin(l.geometry, f.centroid, $fRange)";
+                    $sSQL .= "  AND l.class='".$this->sClass."'";
+                    $sSQL .= "  AND l.type='".$this->sType."'";
+                    $sSQL .= $this->oContext->excludeSQL(' AND l.place_id');
+                    if ($sOrderBySQL) {
+                        $sSQL .= "ORDER BY orderterm ASC";
+                    }
+                    $sSQL .= " limit $iLimit";
+
+                    if (CONST_Debug) var_dump($sSQL);
+
+                    $aClassPlaceIDs = array_merge($aClassPlaceIDs, chksql($oDB->getCol($sSQL)));
+                }
+            }
+        }
+
+        return $aClassPlaceIDs;
+    }
+
+    private function poiTable()
+    {
+        return 'place_classtype_'.$this->sClass.'_'.$this->sType;
+    }
+
+    private function countryCodeSQL($sVar)
+    {
+        if ($this->sCountryCode) {
+            return $sVar.' = \''.$this->sCountryCode."'";
+        }
+        if ($this->oContext->sqlCountryList) {
+            return $sVar.' in '.$this->oContext->sqlCountryList;
+        }
+
+        return '';
+    }
+
+    /////////// Sort functions
+
+
+    public static function bySearchRank($a, $b)
+    {
+        if ($a->iSearchRank == $b->iSearchRank) {
+            return $a->iOperator + strlen($a->sHouseNumber)
+                     - $b->iOperator - strlen($b->sHouseNumber);
+        }
+
+        return $a->iSearchRank < $b->iSearchRank ? -1 : 1;
+    }
+
+    //////////// Debugging functions
+
+
+    public function dumpAsHtmlTableRow(&$aWordIDs)
+    {
+        $kf = function ($k) use (&$aWordIDs) {
+            return $aWordIDs[$k];
+        };
+
+        echo "<tr>";
+        echo "<td>$this->iSearchRank</td>";
+        echo "<td>".join(', ', array_map($kf, $this->aName))."</td>";
+        echo "<td>".join(', ', array_map($kf, $this->aNameNonSearch))."</td>";
+        echo "<td>".join(', ', array_map($kf, $this->aAddress))."</td>";
+        echo "<td>".join(', ', array_map($kf, $this->aAddressNonSearch))."</td>";
+        echo "<td>".$this->sCountryCode."</td>";
+        echo "<td>".Operator::toString($this->iOperator)."</td>";
+        echo "<td>".$this->sClass."</td>";
+        echo "<td>".$this->sType."</td>";
+        echo "<td>".$this->sPostcode."</td>";
+        echo "<td>".$this->sHouseNumber."</td>";
+
+        echo "</tr>";
+    }
+}
diff --git a/lib/SpecialSearchOperator.php b/lib/SpecialSearchOperator.php
new file mode 100644 (file)
index 0000000..a0ec23e
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+
+namespace Nominatim;
+
+/**
+ * Operators describing special searches.
+ */
+abstract class Operator
+{
+    /// No operator selected.
+    const NONE = 0;
+    /// Search for POI of the given type.
+    const TYPE = 1;
+    /// Search for POIs near the given place.
+    const NEAR = 2;
+    /// Search for POIS in the given place.
+    const IN = 3;
+    /// Search for POIS named as given.
+    const NAME = 4;
+    /// Search for postcodes.
+    const POSTCODE = 5;
+
+    private static $aConstantNames = null;
+
+
+    public static function toString($iOperator)
+    {
+        if ($iOperator == Operator::NONE) {
+            return '';
+        }
+
+        if (Operator::$aConstantNames === null) {
+            $oReflector = new \ReflectionClass('Nominatim\Operator');
+            $aConstants = $oReflector->getConstants();
+
+            Operator::$aConstantNames = array();
+            foreach ($aConstants as $sName => $iValue) {
+                Operator::$aConstantNames[$iValue] = $sName;
+            }
+        }
+
+        return Operator::$aConstantNames[$iOperator];
+    }
+}
index 145c57d0b09847b1491bdfe5d72612d6ad192376..01fc344b066efff825ce31007ed567aeac9025a1 100644 (file)
@@ -23,6 +23,11 @@ function getDBQuoted($s)
     return "'".pg_escape_string($s)."'";
 }
 
+function getArraySQL($a)
+{
+    return 'ARRAY['.join(',', $a).']';
+}
+
 function getPostgresVersion(&$oDB)
 {
     $sVersionString = $oDB->getOne('select version()');
index cdd6b5cb773e9ce500959876a0dfd026983d5917..e4a343d15eb8cab208913e56bf17ca5804da4e66 100644 (file)
@@ -51,14 +51,6 @@ function getDatabaseDate(&$oDB)
 }
 
 
-function bySearchRank($a, $b)
-{
-    if ($a['iSearchRank'] == $b['iSearchRank'])
-        return strlen($a['sOperator']) + strlen($a['sHouseNumber']) - strlen($b['sOperator']) - strlen($b['sHouseNumber']);
-    return ($a['iSearchRank'] < $b['iSearchRank']?-1:1);
-}
-
-
 function byImportance($a, $b)
 {
     if ($a['importance'] != $b['importance'])
@@ -489,71 +481,19 @@ function _debugDumpGroupedSearches($aData, $aTokens)
         foreach ($aTokens as $sToken => $aWords) {
             if ($aWords) {
                 foreach ($aWords as $aToken) {
-                    $aWordsIDs[$aToken['word_id']] = $sToken.'('.$aToken['word_id'].')';
+                    $aWordsIDs[$aToken['word_id']] =
+                        '#'.$sToken.'('.$aToken['word_id'].')#';
                 }
             }
         }
     }
     echo "<table border=\"1\">";
     echo "<tr><th>rank</th><th>Name Tokens</th><th>Name Not</th>";
-    echo "<th>Address Tokens</th><th>Address Not</th><th>country</th>";
-    echo "<th>operator</th><th>class</th><th>type</th><th>postcode</th><th>house#</th>";
-    echo "<th>Lat</th><th>Lon</th><th>Radius</th></tr>";
+    echo "<th>Address Tokens</th><th>Address Not</th><th>country</th><th>operator</th>";
+    echo "<th>class</th><th>type</th><th>postcode</th><th>housenumber</th></tr>";
     foreach ($aData as $iRank => $aRankedSet) {
         foreach ($aRankedSet as $aRow) {
-            echo "<tr>";
-            echo "<td>$iRank</td>";
-
-            echo "<td>";
-            $sSep = '';
-            foreach ($aRow['aName'] as $iWordID) {
-                echo $sSep.'#'.$aWordsIDs[$iWordID].'#';
-                $sSep = ', ';
-            }
-            echo "</td>";
-
-            echo "<td>";
-            $sSep = '';
-            foreach ($aRow['aNameNonSearch'] as $iWordID) {
-                echo $sSep.'#'.$aWordsIDs[$iWordID].'#';
-                $sSep = ', ';
-            }
-            echo "</td>";
-
-            echo "<td>";
-            $sSep = '';
-            foreach ($aRow['aAddress'] as $iWordID) {
-                echo $sSep.'#'.$aWordsIDs[$iWordID].'#';
-                $sSep = ', ';
-            }
-            echo "</td>";
-
-            echo "<td>";
-            $sSep = '';
-            foreach ($aRow['aAddressNonSearch'] as $iWordID) {
-                echo $sSep.'#'.$aWordsIDs[$iWordID].'#';
-                $sSep = ', ';
-            }
-            echo "</td>";
-
-            echo "<td>".$aRow['sCountryCode']."</td>";
-
-            echo "<td>".$aRow['sOperator']."</td>";
-            echo "<td>".$aRow['sClass']."</td>";
-            echo "<td>".$aRow['sType']."</td>";
-
-            echo "<td>".$aRow['sPostcode']."</td>";
-            echo "<td>".$aRow['sHouseNumber']."</td>";
-
-            if ($aRow['oNear']) {
-                echo "<td>".$aRow['oNear']->lat()."</td>";
-                echo "<td>".$aRow['oNear']->lon()."</td>";
-                echo "<td>".$aRow['oNear']->radius()."</td>";
-            } else {
-                echo "<td></td><td></td><td></td>";
-            }
-
-            echo "</tr>";
+            $aRow->dumpAsHtmlTableRow($aWordsIDs);
         }
     }
     echo "</table>";
@@ -605,6 +545,81 @@ function addQuotes($s)
     return "'".$s."'";
 }
 
+function parseLatLon($sQuery)
+{
+    $sFound    = null;
+    $fQueryLat = null;
+    $fQueryLon = null;
+
+    if (preg_match('/\\s*([NS])[ ]+([0-9]+[0-9.]*)[° ]+([0-9.]+)?[′\']*[, ]+([EW])[ ]+([0-9]+)[° ]+([0-9]+[0-9.]*)[′\']*\\s*/', $sQuery, $aData)) {
+        /*               1         2                   3                    4         5            6
+         * degrees decimal minutes
+         * N 40 26.767, W 79 58.933
+         * N 40°26.767′, W 79°58.933′
+         */
+        $sFound    = $aData[0];
+        $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2] + $aData[3]/60);
+        $fQueryLon = ($aData[4]=='E'?1:-1) * ($aData[5] + $aData[6]/60);
+    } elseif (preg_match('/\\s*([0-9]+)[° ]+([0-9]+[0-9.]*)?[′\']*[ ]+([NS])[, ]+([0-9]+)[° ]+([0-9]+[0-9.]*)?[′\' ]+([EW])\\s*/', $sQuery, $aData)) {
+        /*                     1            2                         3          4            5                      6
+         * degrees decimal minutes
+         * 40 26.767 N, 79 58.933 W
+         * 40° 26.767′ N 79° 58.933′ W
+         */
+        $sFound    = $aData[0];
+        $fQueryLat = ($aData[3]=='N'?1:-1) * ($aData[1] + $aData[2]/60);
+        $fQueryLon = ($aData[6]=='E'?1:-1) * ($aData[4] + $aData[5]/60);
+    } elseif (preg_match('/\\s*([NS])[ ]([0-9]+)[° ]+([0-9]+)[′\' ]+([0-9]+)[″"]*[, ]+([EW])[ ]([0-9]+)[° ]+([0-9]+)[′\' ]+([0-9]+)[″"]*\\s*/', $sQuery, $aData)) {
+        /*                     1        2            3              4                 5        6            7              8
+         * degrees decimal seconds
+         * N 40 26 46 W 79 58 56
+         * N 40° 26′ 46″, W 79° 58′ 56″
+         */
+        $sFound    = $aData[0];
+        $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2] + $aData[3]/60 + $aData[4]/3600);
+        $fQueryLon = ($aData[5]=='E'?1:-1) * ($aData[6] + $aData[7]/60 + $aData[8]/3600);
+    } elseif (preg_match('/\\s*([0-9]+)[° ]+([0-9]+)[′\' ]+([0-9]+)[″" ]+([NS])[, ]+([0-9]+)[° ]+([0-9]+)[′\' ]+([0-9]+)[″" ]+([EW])\\s*/', $sQuery, $aData)) {
+        /*                     1            2              3             4          5            6              7             8
+         * degrees decimal seconds
+         * 40 26 46 N 79 58 56 W
+         * 40° 26′ 46″ N, 79° 58′ 56″ W
+         */
+        $sFound    = $aData[0];
+        $fQueryLat = ($aData[4]=='N'?1:-1) * ($aData[1] + $aData[2]/60 + $aData[3]/3600);
+        $fQueryLon = ($aData[8]=='E'?1:-1) * ($aData[5] + $aData[6]/60 + $aData[7]/3600);
+    } elseif (preg_match('/\\s*([NS])[ ]([0-9]+[0-9]*\\.[0-9]+)[°]*[, ]+([EW])[ ]([0-9]+[0-9]*\\.[0-9]+)[°]*\\s*/', $sQuery, $aData)) {
+        /*                     1        2                               3        4
+         * degrees decimal
+         * N 40.446° W 79.982°
+         */
+        $sFound    = $aData[0];
+        $fQueryLat = ($aData[1]=='N'?1:-1) * ($aData[2]);
+        $fQueryLon = ($aData[3]=='E'?1:-1) * ($aData[4]);
+    } elseif (preg_match('/\\s*([0-9]+[0-9]*\\.[0-9]+)[° ]+([NS])[, ]+([0-9]+[0-9]*\\.[0-9]+)[° ]+([EW])\\s*/', $sQuery, $aData)) {
+        /*                     1                           2          3                           4
+         * degrees decimal
+         * 40.446° N 79.982° W
+         */
+        $sFound    = $aData[0];
+        $fQueryLat = ($aData[2]=='N'?1:-1) * ($aData[1]);
+        $fQueryLon = ($aData[4]=='E'?1:-1) * ($aData[3]);
+    } elseif (preg_match('/(\\s*\\[|^\\s*|\\s*)(-?[0-9]+[0-9]*\\.[0-9]+)[, ]+(-?[0-9]+[0-9]*\\.[0-9]+)(\\]\\s*|\\s*$|\\s*)/', $sQuery, $aData)) {
+        /*                 1                   2                             3                        4
+         * degrees decimal
+         * 12.34, 56.78
+         * 12.34 56.78
+         * [12.456,-78.90]
+         */
+        $sFound    = $aData[0];
+        $fQueryLat = $aData[2];
+        $fQueryLon = $aData[3];
+    } else {
+        return false;
+    }
+
+    return array($sFound, $fQueryLat, $fQueryLon);
+}
+
 
 function geometryText2Points($geometry_as_text, $fRadius)
 {
index 9f705d83fb54375b3438345150d85377442c86a6..2f4bc11823b89be57f01c1df771f00b33d08beda 100644 (file)
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -10,8 +10,8 @@
 
   <rule ref="Generic.Files.LineLength">
     <properties>
-     <property name="lineLimit" value="199"/>
-     <property name="absoluteLineLimit" value="199"/>
+     <property name="lineLimit" value="194"/>
+     <property name="absoluteLineLimit" value="194"/>
     </properties>
   </rule>
 
index 7c984a542d3770ecaf09534c42a4c1325bf44d35..9df95e03583ddb30d95de716bf9c306448f066cf 100644 (file)
@@ -101,7 +101,6 @@ if (isset($_GET['debug']) && $_GET['debug']) @define('CONST_Debug', true);
 
 @define('CONST_Search_BatchMode', false);
 
-@define('CONST_Search_TryDroppedAddressTerms', false);
 @define('CONST_Search_NameOnlySearchFrequencyThreshold', 500);
 // If set to true, then reverse order of queries will be tried by default.
 // When set to false only selected languages alloow reverse search.
diff --git a/test/php/Nominatim/NearPointTest.php b/test/php/Nominatim/NearPointTest.php
deleted file mode 100644 (file)
index bee7d3e..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-<?php
-
-namespace Nominatim;
-
-require '../../lib/NearPoint.php';
-
-class NearPointTest extends \PHPUnit_Framework_TestCase
-{
-
-
-    protected function setUp()
-    {
-    }
-
-    public function testExtractFromQuery()
-    {
-        // no coordinates expected
-        $this->assertFalse(NearPoint::extractFromQuery(''));
-        $this->assertFalse(NearPoint::extractFromQuery('abc'));
-        $this->assertFalse(NearPoint::extractFromQuery('12 34'));
-        $this->assertFalse(NearPoint::extractFromQuery('200.1 89.9')); // because latitude > 180
-
-        // coordinates expected
-        $this->assertNotNull(NearPoint::extractFromQuery('0.0 -0.0'));
-
-        $aRes = NearPoint::extractFromQuery(' abc 12.456 -78.90 def ');
-        $this->assertEquals($aRes['pt']->lat(), 12.456);
-        $this->assertEquals($aRes['pt']->lon(), -78.90);
-        $this->assertEquals($aRes['pt']->radius(), 0.1);
-        $this->assertEquals($aRes['query'], 'abc def');
-
-        $aRes = NearPoint::extractFromQuery(' [12.456,-78.90] ');
-        $this->assertEquals($aRes['pt']->lat(), 12.456);
-        $this->assertEquals($aRes['pt']->lon(), -78.90);
-        $this->assertEquals($aRes['pt']->radius(), 0.1);
-        $this->assertEquals($aRes['query'], '');
-
-        $aRes = NearPoint::extractFromQuery(' -12.456,-78.90 ');
-        $this->assertEquals($aRes['pt']->lat(), -12.456);
-        $this->assertEquals($aRes['pt']->lon(), -78.90);
-
-        // http://en.wikipedia.org/wiki/Geographic_coordinate_conversion
-        // these all represent the same location
-        $aQueries = array(
-                     '40 26.767 N 79 58.933 W',
-                     '40° 26.767′ N 79° 58.933′ W',
-                     "40° 26.767' N 79° 58.933' W",
-                     'N 40 26.767, W 79 58.933',
-                     'N 40°26.767′, W 79°58.933′',
-                     "N 40°26.767', W 79°58.933'",
-                     '40 26 46 N 79 58 56 W',
-                     '40° 26′ 46″ N 79° 58′ 56″ W',
-                     'N 40 26 46 W 79 58 56',
-                     'N 40° 26′ 46″, W 79° 58′ 56″',
-                     'N 40° 26\' 46", W 79° 58\' 56"',
-                     '40.446 -79.982',
-                     '40.446,-79.982',
-                     '40.446° N 79.982° W',
-                     'N 40.446° W 79.982°',
-                     '[40.446 -79.982]',
-                     '       40.446  ,   -79.982     ',
-                    );
-
-
-        foreach ($aQueries as $sQuery) {
-            $aRes = NearPoint::extractFromQuery($sQuery);
-            $this->assertEquals(40.446, $aRes['pt']->lat(), 'degrees decimal ' . $sQuery, 0.01);
-            $this->assertEquals(-79.982, $aRes['pt']->lon(), 'degrees decimal ' . $sQuery, 0.01);
-            $this->assertEquals('', $aRes['query']);
-        }
-    }
-
-    public function testWithinSQL()
-    {
-        $np = new NearPoint(0.1, 23, 1);
-
-        $this->assertEquals(
-            'ST_DWithin(foo, ST_SetSRID(ST_Point(23,0.1),4326), 1.000000)',
-            $np->withinSQL('foo')
-        );
-    }
-
-    public function testDistanceSQL()
-    {
-        $np = new NearPoint(0.1, 23, 1);
-
-        $this->assertEquals(
-            'ST_Distance(ST_SetSRID(ST_Point(23,0.1),4326), foo)',
-            $np->distanceSQL('foo')
-        );
-    }
-}
index a651fe40ca130102e893cc0382f3c8e9e5a16f46..33bb6d32037a3be0b545a2c7931777431b9c7968 100644 (file)
@@ -2,7 +2,7 @@
 
 namespace Nominatim;
 
-require '../../lib/lib.php';
+require_once '../../lib/lib.php';
 
 class NominatimTest extends \PHPUnit_Framework_TestCase
 {
@@ -203,4 +203,63 @@ class NominatimTest extends \PHPUnit_Framework_TestCase
             geometryText2Points('MULTIPOLYGON(((30 20, 45 40, 10 40, 30 20)),((15 5, 40 10, 10 20, 5 10, 15 5)))', $fRadius)
         );
     }
+
+    public function testParseLatLon()
+    {
+        // no coordinates expected
+        $this->assertFalse(parseLatLon(''));
+        $this->assertFalse(parseLatLon('abc'));
+        $this->assertFalse(parseLatLon('12 34'));
+
+        // coordinates expected
+        $this->assertNotNull(parseLatLon('0.0 -0.0'));
+
+        $aRes = parseLatLon(' abc 12.456 -78.90 def ');
+        $this->assertEquals($aRes[1], 12.456);
+        $this->assertEquals($aRes[2], -78.90);
+        $this->assertEquals($aRes[0], ' 12.456 -78.90 ');
+
+        $aRes = parseLatLon(' [12.456,-78.90] ');
+        $this->assertEquals($aRes[1], 12.456);
+        $this->assertEquals($aRes[2], -78.90);
+        $this->assertEquals($aRes[0], ' [12.456,-78.90] ');
+
+        $aRes = parseLatLon(' -12.456,-78.90 ');
+        $this->assertEquals($aRes[1], -12.456);
+        $this->assertEquals($aRes[2], -78.90);
+        $this->assertEquals($aRes[0], ' -12.456,-78.90 ');
+
+        // http://en.wikipedia.org/wiki/Geographic_coordinate_conversion
+        // these all represent the same location
+        $aQueries = array(
+                     '40 26.767 N 79 58.933 W',
+                     '40° 26.767′ N 79° 58.933′ W',
+                     "40° 26.767' N 79° 58.933' W",
+                     'N 40 26.767, W 79 58.933',
+                     'N 40°26.767′, W 79°58.933′',
+                     "N 40°26.767', W 79°58.933'",
+                     '40 26 46 N 79 58 56 W',
+                     '40° 26′ 46″ N 79° 58′ 56″ W',
+                     'N 40 26 46 W 79 58 56',
+                     'N 40° 26′ 46″, W 79° 58′ 56″',
+                     'N 40° 26\' 46", W 79° 58\' 56"',
+                     '40.446 -79.982',
+                     '40.446,-79.982',
+                     '40.446° N 79.982° W',
+                     'N 40.446° W 79.982°',
+                     '[40.446 -79.982]',
+                     '       40.446  ,   -79.982     ',
+                    );
+
+
+        foreach ($aQueries as $sQuery) {
+            $aRes = parseLatLon($sQuery);
+            $this->assertEquals(40.446, $aRes[1], 'degrees decimal ' . $sQuery, 0.01);
+            $this->assertEquals(-79.982, $aRes[2], 'degrees decimal ' . $sQuery, 0.01);
+            $this->assertEquals($sQuery, $aRes[0]);
+        }
+    }
 }
diff --git a/test/php/Nominatim/SearchContextTest.php b/test/php/Nominatim/SearchContextTest.php
new file mode 100644 (file)
index 0000000..a40d071
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+
+namespace Nominatim;
+
+@define('CONST_BasePath', '../../');
+
+require_once '../../lib/SearchContext.php';
+
+class SearchContextTest extends \PHPUnit_Framework_TestCase
+{
+    private $oCtx;
+
+    protected function setUp()
+    {
+        $this->oCtx = new SearchContext();
+    }
+
+    public function testHasNearPoint()
+    {
+        $this->assertFalse($this->oCtx->hasNearPoint());
+        $this->oCtx->setNearPoint(0, 0);
+        $this->assertTrue($this->oCtx->hasNearPoint());
+    }
+
+    public function testNearRadius()
+    {
+        $this->oCtx->setNearPoint(1, 1);
+        $this->assertEquals(0.1, $this->oCtx->nearRadius());
+        $this->oCtx->setNearPoint(1, 1, 0.338);
+        $this->assertEquals(0.338, $this->oCtx->nearRadius());
+    }
+
+    public function testWithinSQL()
+    {
+        $this->oCtx->setNearPoint(0.1, 23, 1);
+
+        $this->assertEquals(
+            'ST_DWithin(foo, ST_SetSRID(ST_Point(23,0.1),4326), 1.000000)',
+            $this->oCtx->withinSQL('foo')
+        );
+    }
+
+    public function testDistanceSQL()
+    {
+        $this->oCtx->setNearPoint(0.1, 23, 1);
+
+        $this->assertEquals(
+            'ST_Distance(ST_SetSRID(ST_Point(23,0.1),4326), foo)',
+            $this->oCtx->distanceSQL('foo')
+        );
+    }
+}
index 8663019624087bee395b8e27b7a4b6f5c5ee0476..89177914d8948cdb7cf189e16d2aef393d10dce1 100755 (executable)
@@ -540,7 +540,6 @@ if ($aCMDResult['calculate-postcodes'] || $aCMDResult['all']) {
         $sSQL .= "  FROM us_postcode WHERE postcode NOT IN";
         $sSQL .= "        (SELECT postcode FROM location_postcode";
         $sSQL .= "          WHERE country_code = 'us')";
-
     } else {
         $sSQL .= "TRUNCATE TABLE us_postcode";
     }
index 334ad123c41445eb0e9c4dce6435128125dcb952..3026dacb5b0ecb235f51a86881dedf5b1b309358 100755 (executable)
@@ -78,8 +78,7 @@ if ($aResult['init-updates']) {
     if ($sDatabaseDate === false) {
         fail("Cannot determine date of database.");
     }
-    $sWindBack = strftime('%Y-%m-%dT%H:%M:%SZ',
-                          strtotime($sDatabaseDate) - (3*60*60));
+    $sWindBack = strftime('%Y-%m-%dT%H:%M:%SZ', strtotime($sDatabaseDate) - (3*60*60));
 
     // get the appropriate state id
     $aOutput = 0;
@@ -289,7 +288,7 @@ if ($aResult['import-osmosis'] || $aResult['import-osmosis-all']) {
                 if ($iResult == 3) {
                     echo 'No new updates. Sleeping for '.CONST_Replication_Recheck_Interval." sec.\n";
                     sleep(CONST_Replication_Recheck_Interval);
-                } else if ($iResult != 0) {
+                } elseif ($iResult != 0) {
                     echo 'ERROR: updates failed.';
                     exit($iResult);
                 } else {
@@ -326,7 +325,11 @@ if ($aResult['import-osmosis'] || $aResult['import-osmosis-all']) {
 
             // write the update logs
             $iFileSize = filesize($sImportFile);
-            $sSQL = "INSERT INTO import_osmosis_log (batchend, batchseq, batchsize, starttime, endtime, event) values ('$sBatchEnd',$iEndSequence,$iFileSize,'".date('Y-m-d H:i:s', $fCMDStartTime)."','".date('Y-m-d H:i:s')."','import')";
+            $sSQL = 'INSERT INTO import_osmosis_log';
+            $sSQL .= '(batchend, batchseq, batchsize, starttime, endtime, event)';
+            $sSQL .= " values ('$sBatchEnd',$iEndSequence,$iFileSize,'";
+            $sSQL .= date('Y-m-d H:i:s', $fCMDStartTime)."','";
+            $sSQL .= date('Y-m-d H:i:s')."','import')";
             var_Dump($sSQL);
             chksql($oDB->query($sSQL));
 
@@ -349,7 +352,11 @@ if ($aResult['import-osmosis'] || $aResult['import-osmosis-all']) {
                 exit($iErrorLevel);
             }
 
-            $sSQL = "INSERT INTO import_osmosis_log (batchend, batchseq, batchsize, starttime, endtime, event) values ('$sBatchEnd',$iEndSequence,$iFileSize,'".date('Y-m-d H:i:s', $fCMDStartTime)."','".date('Y-m-d H:i:s')."','index')";
+            $sSQL = 'INSERT INTO import_osmosis_log';
+            $sSQL .= '(batchend, batchseq, batchsize, starttime, endtime, event)';
+            $sSQL .= " values ('$sBatchEnd',$iEndSequence,$iFileSize,'";
+            $sSQL .= date('Y-m-d H:i:s', $fCMDStartTime)."','";
+            $sSQL .= date('Y-m-d H:i:s')."','index')";
             var_Dump($sSQL);
             $oDB->query($sSQL);
             echo date('Y-m-d H:i:s')." Completed index step for $sBatchEnd in ".round((time()-$fCMDStartTime)/60, 2)." minutes\n";
@@ -363,4 +370,3 @@ if ($aResult['import-osmosis'] || $aResult['import-osmosis-all']) {
         if (!$aResult['import-osmosis-all']) exit(0);
     }
 }
-