]> 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)
1  2 
lib/Geocode.php
lib/lib.php
utils/update.php

diff --combined lib/Geocode.php
index 15915f940133273991916093a6d145e75cbe8ece,e02aae9491aa9033a5698e8cdfc46b6cd55cf8f4..4b2ae66226c44a67d731949e166d9f9c48e23fdc
@@@ -2,9 -2,10 +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
  {
@@@ -25,7 -26,7 +26,7 @@@
  
      protected $aExcludePlaceIDs = array();
      protected $bDeDupe = true;
 -    protected $bReverseInPlan = false;
 +    protected $bReverseInPlan = true;
  
      protected $iLimit = 20;
      protected $iFinalLimit = 10;
@@@ -36,9 -37,8 +37,8 @@@
  
      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;
          $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);
          ) {
              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)
                  $aRoute = $oParams->getStringList('route');
                  $fRouteWidth = $oParams->getFloat('routewidth');
                  if ($aRoute && $fRouteWidth) {
-                     $this->setRoute($aRoute, $fRouteWidth);
+                     $this->aRoutePoints = $aRoute;
+                     $this->aRouteWidth = $fRouteWidth;
                  }
              }
          }
          $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);
          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,";
          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 ";
          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 ";
                  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 ";
              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)) ";
                  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 ";
  
          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
                      //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) {
              // 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);
              //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);
  
      {
          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.");
              $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;
  
                  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) {
  
                      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;
                          }
                      }
  
                  // 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);
  
              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
                      $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 --combined lib/lib.php
index cdd6b5cb773e9ce500959876a0dfd026983d5917,b5fbee3e600b08548119d7e076a40fd3a77dd4ed..e4a343d15eb8cab208913e56bf17ca5804da4e66
@@@ -51,14 -51,6 +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 +481,19 @@@ function _debugDumpGroupedSearches($aDa
          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 +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)
  {
          //
          preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/', $aMatch[1], $aPolyPoints, PREG_SET_ORDER);
          //
 -    } elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#', $geometry_as_text, $aMatch)) {
 +/*    } elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#', $geometry_as_text, $aMatch)) {
          //
          preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/', $aMatch[1], $aPolyPoints, PREG_SET_ORDER);
 -        //
 +        */
      } elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#', $geometry_as_text, $aMatch)) {
          //
          $aPolyPoints = createPointsAroundCenter($aMatch[1], $aMatch[2], $fRadius);
diff --combined utils/update.php
index 334ad123c41445eb0e9c4dce6435128125dcb952,d729519a3ed5f3c9eb0d4f267094a702b7001eca..3026dacb5b0ecb235f51a86881dedf5b1b309358
@@@ -38,7 -38,6 +38,7 @@@ $aCMDOption
  getCmdOpt($_SERVER['argv'], $aCMDOptions, $aResult, true, true);
  
  if (!isset($aResult['index-instances'])) $aResult['index-instances'] = 1;
 +
  if (!isset($aResult['index-rank'])) $aResult['index-rank'] = 0;
  
  date_default_timezone_set('Etc/UTC');
@@@ -78,8 -77,7 +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 -287,7 +288,7 @@@ if ($aResult['import-osmosis'] || $aRes
                  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 {
  
              // 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));
  
                  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";
          if (!$aResult['import-osmosis-all']) exit(0);
      }
  }