]> git.openstreetmap.org Git - nominatim.git/blob - lib/Geocode.php
remove check for class/type shortcuts which were never implemented
[nominatim.git] / lib / Geocode.php
1 <?php
2         class Geocode
3         {
4                 protected $oDB;
5
6                 protected $aLangPrefOrder = array();
7
8                 protected $bIncludeAddressDetails = false;
9
10                 protected $bIncludePolygonAsPoints = false;
11                 protected $bIncludePolygonAsText = false;
12                 protected $bIncludePolygonAsGeoJSON = false;
13                 protected $bIncludePolygonAsKML = false;
14                 protected $bIncludePolygonAsSVG = false;
15
16                 protected $aExcludePlaceIDs = array();
17                 protected $bDeDupe = true;
18                 protected $bReverseInPlan = false;
19
20                 protected $iLimit = 20;
21                 protected $iFinalLimit = 10;
22                 protected $iOffset = 0;
23                 protected $bFallback = false;
24
25                 protected $aCountryCodes = false;
26                 protected $aNearPoint = false;
27
28                 protected $bBoundedSearch = false;
29                 protected $aViewBox = false;
30                 protected $sViewboxSmallSQL = false;
31                 protected $sViewboxLargeSQL = false;
32                 protected $aRoutePoints = false;
33
34                 protected $iMaxRank = 20;
35                 protected $iMinAddressRank = 0;
36                 protected $iMaxAddressRank = 30;
37                 protected $aAddressRankList = array();
38                 protected $exactMatchCache = array();
39
40                 protected $sAllowedTypesSQLList = false;
41
42                 protected $sQuery = false;
43                 protected $aStructuredQuery = false;
44
45                 function Geocode(&$oDB)
46                 {
47                         $this->oDB =& $oDB;
48                 }
49
50                 function setReverseInPlan($bReverse)
51                 {
52                         $this->bReverseInPlan = $bReverse;
53                 }
54
55                 function setLanguagePreference($aLangPref)
56                 {
57                         $this->aLangPrefOrder = $aLangPref;
58                 }
59
60                 function setIncludeAddressDetails($bAddressDetails = true)
61                 {
62                         $this->bIncludeAddressDetails = (bool)$bAddressDetails;
63                 }
64
65                 function getIncludeAddressDetails()
66                 {
67                         return $this->bIncludeAddressDetails;
68                 }
69
70                 function setIncludePolygonAsPoints($b = true)
71                 {
72                         $this->bIncludePolygonAsPoints = $b;
73                 }
74
75                 function getIncludePolygonAsPoints()
76                 {
77                         return $this->bIncludePolygonAsPoints;
78                 }
79
80                 function setIncludePolygonAsText($b = true)
81                 {
82                         $this->bIncludePolygonAsText = $b;
83                 }
84
85                 function getIncludePolygonAsText()
86                 {
87                         return $this->bIncludePolygonAsText;
88                 }
89
90                 function setIncludePolygonAsGeoJSON($b = true)
91                 {
92                         $this->bIncludePolygonAsGeoJSON = $b;
93                 }
94
95                 function setIncludePolygonAsKML($b = true)
96                 {
97                         $this->bIncludePolygonAsKML = $b;
98                 }
99
100                 function setIncludePolygonAsSVG($b = true)
101                 {
102                         $this->bIncludePolygonAsSVG = $b;
103                 }
104
105                 function setDeDupe($bDeDupe = true)
106                 {
107                         $this->bDeDupe = (bool)$bDeDupe;
108                 }
109
110                 function setLimit($iLimit = 10)
111                 {
112                         if ($iLimit > 50) $iLimit = 50;
113                         if ($iLimit < 1) $iLimit = 1;
114
115                         $this->iFinalLimit = $iLimit;
116                         $this->iLimit = $this->iFinalLimit + min($this->iFinalLimit, 10);
117                 }
118
119                 function setOffset($iOffset = 0)
120                 {
121                         $this->iOffset = $iOffset;
122                 }
123
124                 function setFallback($bFallback = true)
125                 {
126                         $this->bFallback = (bool)$bFallback;
127                 }
128
129                 function setExcludedPlaceIDs($a)
130                 {
131                         // TODO: force to int
132                         $this->aExcludePlaceIDs = $a;
133                 }
134
135                 function getExcludedPlaceIDs()
136                 {
137                         return $this->aExcludePlaceIDs;
138                 }
139
140                 function setBounded($bBoundedSearch = true)
141                 {
142                         $this->bBoundedSearch = (bool)$bBoundedSearch;
143                 }
144
145                 function setViewBox($fLeft, $fBottom, $fRight, $fTop)
146                 {
147                         $this->aViewBox = array($fLeft, $fBottom, $fRight, $fTop);
148                 }
149
150                 function getViewBoxString()
151                 {
152                         if (!$this->aViewBox) return null;
153                         return $this->aViewBox[0].','.$this->aViewBox[3].','.$this->aViewBox[2].','.$this->aViewBox[1];
154                 }
155
156                 function setRoute($aRoutePoints)
157                 {
158                         $this->aRoutePoints = $aRoutePoints;
159                 }
160
161                 function setFeatureType($sFeatureType)
162                 {
163                         switch($sFeatureType)
164                         {
165                         case 'country':
166                                 $this->setRankRange(4, 4);
167                                 break;
168                         case 'state':
169                                 $this->setRankRange(8, 8);
170                                 break;
171                         case 'city':
172                                 $this->setRankRange(14, 16);
173                                 break;
174                         case 'settlement':
175                                 $this->setRankRange(8, 20);
176                                 break;
177                         }
178                 }
179
180                 function setRankRange($iMin, $iMax)
181                 {
182                         $this->iMinAddressRank = (int)$iMin;
183                         $this->iMaxAddressRank = (int)$iMax;
184                 }
185
186                 function setNearPoint($aNearPoint, $fRadiusDeg = 0.1)
187                 {
188                         $this->aNearPoint = array((float)$aNearPoint[0], (float)$aNearPoint[1], (float)$fRadiusDeg);
189                 }
190
191                 function setCountryCodesList($aCountryCodes)
192                 {
193                         $this->aCountryCodes = $aCountryCodes;
194                 }
195
196                 function setQuery($sQueryString)
197                 {
198                         $this->sQuery = $sQueryString;
199                         $this->aStructuredQuery = false;
200                 }
201
202                 function getQueryString()
203                 {
204                         return $this->sQuery;
205                 }
206
207
208                 function loadParamArray($aParams)
209                 {
210                         if (isset($aParams['addressdetails'])) $this->bIncludeAddressDetails = (bool)$aParams['addressdetails'];
211                         if (isset($aParams['bounded'])) $this->bBoundedSearch = (bool)$aParams['bounded'];
212                         if (isset($aParams['dedupe'])) $this->bDeDupe = (bool)$aParams['dedupe'];
213
214                         if (isset($aParams['limit'])) $this->setLimit((int)$aParams['limit']);
215                         if (isset($aParams['offset'])) $this->iOffset = (int)$aParams['offset'];
216
217                         if (isset($aParams['fallback'])) $this->bFallback = (bool)$aParams['fallback'];
218
219                         // List of excluded Place IDs - used for more acurate pageing
220                         if (isset($aParams['exclude_place_ids']) && $aParams['exclude_place_ids'])
221                         {
222                                 foreach(explode(',',$aParams['exclude_place_ids']) as $iExcludedPlaceID)
223                                 {
224                                         $iExcludedPlaceID = (int)$iExcludedPlaceID;
225                                         if ($iExcludedPlaceID)
226                                                 $aExcludePlaceIDs[$iExcludedPlaceID] = $iExcludedPlaceID;
227                                 }
228
229                                 if (isset($aExcludePlaceIDs))
230                                         $this->aExcludePlaceIDs = $aExcludePlaceIDs;
231                         }
232
233                         // Only certain ranks of feature
234                         if (isset($aParams['featureType'])) $this->setFeatureType($aParams['featureType']);
235                         if (isset($aParams['featuretype'])) $this->setFeatureType($aParams['featuretype']);
236
237                         // Country code list
238                         if (isset($aParams['countrycodes']))
239                         {
240                                 $aCountryCodes = array();
241                                 foreach(explode(',',$aParams['countrycodes']) as $sCountryCode)
242                                 {
243                                         if (preg_match('/^[a-zA-Z][a-zA-Z]$/', $sCountryCode))
244                                         {
245                                                 $aCountryCodes[] = strtolower($sCountryCode);
246                                         }
247                                 }
248                                 $this->aCountryCodes = $aCountryCodes;
249                         }
250
251                         if (isset($aParams['viewboxlbrt']) && $aParams['viewboxlbrt'])
252                         {
253                                 $aCoOrdinatesLBRT = explode(',',$aParams['viewboxlbrt']);
254                                 $this->setViewBox($aCoOrdinatesLBRT[0], $aCoOrdinatesLBRT[1], $aCoOrdinatesLBRT[2], $aCoOrdinatesLBRT[3]);
255                         }
256                         else if (isset($aParams['viewbox']) && $aParams['viewbox'])
257                         {
258                                 $aCoOrdinatesLTRB = explode(',',$aParams['viewbox']);
259                                 $this->setViewBox($aCoOrdinatesLTRB[0], $aCoOrdinatesLTRB[3], $aCoOrdinatesLTRB[2], $aCoOrdinatesLTRB[1]);
260                         }
261
262                         if (isset($aParams['route']) && $aParams['route'] && isset($aParams['routewidth']) && $aParams['routewidth'])
263                         {
264                                 $aPoints = explode(',',$aParams['route']);
265                                 if (sizeof($aPoints) % 2 != 0)
266                                 {
267                                         userError("Uneven number of points");
268                                         exit;
269                                 }
270                                 $fPrevCoord = false;
271                                 $aRoute = array();
272                                 foreach($aPoints as $i => $fPoint)
273                                 {
274                                         if ($i%2)
275                                         {
276                                                 $aRoute[] = array((float)$fPoint, $fPrevCoord);
277                                         }
278                                         else
279                                         {
280                                                 $fPrevCoord = (float)$fPoint;
281                                         }
282                                 }
283                                 $this->aRoutePoints = $aRoute;
284                         }
285                 }
286
287                 function setQueryFromParams($aParams)
288                 {
289                         // Search query
290                         $sQuery = (isset($aParams['q'])?trim($aParams['q']):'');
291                         if (!$sQuery)
292                         {
293                                 $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']);
294                                 $this->setReverseInPlan(false);
295                         }
296                         else
297                         {
298                                 $this->setQuery($sQuery);
299                         }
300                 }
301
302                 function loadStructuredAddressElement($sValue, $sKey, $iNewMinAddressRank, $iNewMaxAddressRank, $aItemListValues)
303                 {
304                         $sValue = trim($sValue);
305                         if (!$sValue) return false;
306                         $this->aStructuredQuery[$sKey] = $sValue;
307                         if ($this->iMinAddressRank == 0 && $this->iMaxAddressRank == 30)
308                         {
309                                 $this->iMinAddressRank = $iNewMinAddressRank;
310                                 $this->iMaxAddressRank = $iNewMaxAddressRank;
311                         }
312                         if ($aItemListValues) $this->aAddressRankList = array_merge($this->aAddressRankList, $aItemListValues);
313                         return true;
314                 }
315
316                 function setStructuredQuery($sAmentiy = false, $sStreet = false, $sCity = false, $sCounty = false, $sState = false, $sCountry = false, $sPostalCode = false)
317                 {
318                         $this->sQuery = false;
319
320                         // Reset
321                         $this->iMinAddressRank = 0;
322                         $this->iMaxAddressRank = 30;
323                         $this->aAddressRankList = array();
324
325                         $this->aStructuredQuery = array();
326                         $this->sAllowedTypesSQLList = '';
327
328                         $this->loadStructuredAddressElement($sAmentiy, 'amenity', 26, 30, false);
329                         $this->loadStructuredAddressElement($sStreet, 'street', 26, 30, false);
330                         $this->loadStructuredAddressElement($sCity, 'city', 14, 24, false);
331                         $this->loadStructuredAddressElement($sCounty, 'county', 9, 13, false);
332                         $this->loadStructuredAddressElement($sState, 'state', 8, 8, false);
333                         $this->loadStructuredAddressElement($sPostalCode, 'postalcode' , 5, 11, array(5, 11));
334                         $this->loadStructuredAddressElement($sCountry, 'country', 4, 4, false);
335
336                         if (sizeof($this->aStructuredQuery) > 0) 
337                         {
338                                 $this->sQuery = join(', ', $this->aStructuredQuery);
339                                 if ($this->iMaxAddressRank < 30)
340                                 {
341                                         $sAllowedTypesSQLList = '(\'place\',\'boundary\')';
342                                 }
343                         }
344                 }
345
346                 function fallbackStructuredQuery()
347                 {
348                         if (!$this->aStructuredQuery) return false;
349
350                         $aParams = $this->aStructuredQuery;
351
352                         if (sizeof($aParams) == 1) return false;
353
354                         $aOrderToFallback = array('postalcode', 'street', 'city', 'county', 'state');
355
356                         foreach($aOrderToFallback as $sType)
357                         {
358                                 if (isset($aParams[$sType]))
359                                 {
360                                         unset($aParams[$sType]);
361                                         $this->setStructuredQuery(@$aParams['amenity'], @$aParams['street'], @$aParams['city'], @$aParams['county'], @$aParams['state'], @$aParams['country'], @$aParams['postalcode']);
362                                         return true;
363                                 }
364                         }
365
366                         return false;
367                 }
368
369                 function getDetails($aPlaceIDs)
370                 {
371                         if (sizeof($aPlaceIDs) == 0)  return array();
372
373                         $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
374
375                         // Get the details for display (is this a redundant extra step?)
376                         $sPlaceIDs = join(',',$aPlaceIDs);
377
378                         $sImportanceSQL = '';
379                         if ($this->sViewboxSmallSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxSmallSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
380                         if ($this->sViewboxLargeSQL) $sImportanceSQL .= " case when ST_Contains($this->sViewboxLargeSQL, ST_Collect(centroid)) THEN 1 ELSE 0.75 END * ";
381
382                         $sSQL = "select osm_type,osm_id,class,type,admin_level,rank_search,rank_address,min(place_id) as place_id, min(parent_place_id) as parent_place_id, calculated_country_code as country_code,";
383                         $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
384                         $sSQL .= "get_name_by_language(name, $sLanguagePrefArraySQL) as placename,";
385                         $sSQL .= "get_name_by_language(name, ARRAY['ref']) as ref,";
386                         $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
387                         $sSQL .= $sImportanceSQL."coalesce(importance,0.75-(rank_search::float/40)) as importance, ";
388                         $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(CASE WHEN placex.rank_search < 28 THEN placex.place_id ELSE placex.parent_place_id END) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
389                         $sSQL .= "(extratags->'place') as extra_place ";
390                         $sSQL .= "from placex where place_id in ($sPlaceIDs) ";
391                         $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
392                         if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
393                         if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
394                         $sSQL .= ") ";
395                         if ($this->sAllowedTypesSQLList) $sSQL .= "and placex.class in $this->sAllowedTypesSQLList ";
396                         $sSQL .= "and linked_place_id is null ";
397                         $sSQL .= "group by osm_type,osm_id,class,type,admin_level,rank_search,rank_address,calculated_country_code,importance";
398                         if (!$this->bDeDupe) $sSQL .= ",place_id";
399                         $sSQL .= ",langaddress ";
400                         $sSQL .= ",placename ";
401                         $sSQL .= ",ref ";
402                         $sSQL .= ",extratags->'place' ";
403
404                         if (30 >= $this->iMinAddressRank && 30 <= $this->iMaxAddressRank)
405                         {
406                                 $sSQL .= " union ";
407                                 $sSQL .= "select 'T' as osm_type,place_id as osm_id,'place' as class,'house' as type,null as admin_level,30 as rank_search,30 as rank_address,min(place_id) as place_id, min(parent_place_id) as parent_place_id,'us' as country_code,";
408                                 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
409                                 $sSQL .= "null as placename,";
410                                 $sSQL .= "null as ref,";
411                                 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
412                                 $sSQL .= $sImportanceSQL."-1.15 as importance, ";
413                                 $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(location_property_tiger.parent_place_id) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
414                                 $sSQL .= "null as extra_place ";
415                                 $sSQL .= "from location_property_tiger where place_id in ($sPlaceIDs) ";
416                                 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
417                                 $sSQL .= "group by place_id";
418                                 if (!$this->bDeDupe) $sSQL .= ",place_id ";
419                                 $sSQL .= " union ";
420                                 $sSQL .= "select 'L' as osm_type,place_id as osm_id,'place' as class,'house' as type,null as admin_level,30 as rank_search,30 as rank_address,min(place_id) as place_id, min(parent_place_id) as parent_place_id,'us' as country_code,";
421                                 $sSQL .= "get_address_by_language(place_id, $sLanguagePrefArraySQL) as langaddress,";
422                                 $sSQL .= "null as placename,";
423                                 $sSQL .= "null as ref,";
424                                 $sSQL .= "avg(ST_X(centroid)) as lon,avg(ST_Y(centroid)) as lat, ";
425                                 $sSQL .= $sImportanceSQL."-1.10 as importance, ";
426                                 $sSQL .= "(select max(p.importance*(p.rank_address+2)) from place_addressline s, placex p where s.place_id = min(location_property_aux.parent_place_id) and p.place_id = s.address_place_id and s.isaddress and p.importance is not null) as addressimportance, ";
427                                 $sSQL .= "null as extra_place ";
428                                 $sSQL .= "from location_property_aux where place_id in ($sPlaceIDs) ";
429                                 $sSQL .= "and 30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
430                                 $sSQL .= "group by place_id";
431                                 if (!$this->bDeDupe) $sSQL .= ",place_id";
432                                 $sSQL .= ",get_address_by_language(place_id, $sLanguagePrefArraySQL) ";
433                         }
434
435                         $sSQL .= " order by importance desc";
436                         if (CONST_Debug) { echo "<hr>"; var_dump($sSQL); }
437                         $aSearchResults = $this->oDB->getAll($sSQL);
438
439                         if (PEAR::IsError($aSearchResults))
440                         {
441                                 failInternalError("Could not get details for place.", $sSQL, $aSearchResults);
442                         }
443
444                         return $aSearchResults;
445                 }
446
447                 function getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases)
448                 {
449                         /*
450                            Calculate all searches using aValidTokens i.e.
451                            'Wodsworth Road, Sheffield' =>
452
453                            Phrase Wordset
454                            0      0       (wodsworth road)
455                            0      1       (wodsworth)(road)
456                            1      0       (sheffield)
457
458                            Score how good the search is so they can be ordered
459                          */
460                         foreach($aPhrases as $iPhrase => $sPhrase)
461                         {
462                                 $aNewPhraseSearches = array();
463                                 if ($bStructuredPhrases) $sPhraseType = $aPhraseTypes[$iPhrase];
464                                 else $sPhraseType = '';
465
466                                 foreach($aPhrases[$iPhrase]['wordsets'] as $iWordSet => $aWordset)
467                                 {
468                                         // Too many permutations - too expensive
469                                         if ($iWordSet > 120) break;
470
471                                         $aWordsetSearches = $aSearches;
472
473                                         // Add all words from this wordset
474                                         foreach($aWordset as $iToken => $sToken)
475                                         {
476                                                 //echo "<br><b>$sToken</b>";
477                                                 $aNewWordsetSearches = array();
478
479                                                 foreach($aWordsetSearches as $aCurrentSearch)
480                                                 {
481                                                         //echo "<i>";
482                                                         //var_dump($aCurrentSearch);
483                                                         //echo "</i>";
484
485                                                         // If the token is valid
486                                                         if (isset($aValidTokens[' '.$sToken]))
487                                                         {
488                                                                 foreach($aValidTokens[' '.$sToken] as $aSearchTerm)
489                                                                 {
490                                                                         $aSearch = $aCurrentSearch;
491                                                                         $aSearch['iSearchRank']++;
492                                                                         if (($sPhraseType == '' || $sPhraseType == 'country') && !empty($aSearchTerm['country_code']) && $aSearchTerm['country_code'] != '0')
493                                                                         {
494                                                                                 if ($aSearch['sCountryCode'] === false)
495                                                                                 {
496                                                                                         $aSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
497                                                                                         // Country is almost always at the end of the string - increase score for finding it anywhere else (optimisation)
498                                                                                         if (($iToken+1 != sizeof($aWordset) || $iPhrase+1 != sizeof($aPhrases)))
499                                                                                         {
500                                                                                                 $aSearch['iSearchRank'] += 5;
501                                                                                         }
502                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
503                                                                                 }
504                                                                         }
505                                                                         elseif (isset($aSearchTerm['lat']) && $aSearchTerm['lat'] !== '' && $aSearchTerm['lat'] !== null)
506                                                                         {
507                                                                                 if ($aSearch['fLat'] === '')
508                                                                                 {
509                                                                                         $aSearch['fLat'] = $aSearchTerm['lat'];
510                                                                                         $aSearch['fLon'] = $aSearchTerm['lon'];
511                                                                                         $aSearch['fRadius'] = $aSearchTerm['radius'];
512                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
513                                                                                 }
514                                                                         }
515                                                                         elseif ($sPhraseType == 'postalcode')
516                                                                         {
517                                                                                 // 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
518                                                                                 if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
519                                                                                 {
520                                                                                         // If we already have a name try putting the postcode first
521                                                                                         if (sizeof($aSearch['aName']))
522                                                                                         {
523                                                                                                 $aNewSearch = $aSearch;
524                                                                                                 $aNewSearch['aAddress'] = array_merge($aNewSearch['aAddress'], $aNewSearch['aName']);
525                                                                                                 $aNewSearch['aName'] = array();
526                                                                                                 $aNewSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
527                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aNewSearch;
528                                                                                         }
529
530                                                                                         if (sizeof($aSearch['aName']))
531                                                                                         {
532                                                                                                 if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strpos($sToken, ' ') !== false))
533                                                                                                 {
534                                                                                                         $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
535                                                                                                 }
536                                                                                                 else
537                                                                                                 {
538                                                                                                         $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
539                                                                                                         $aSearch['iSearchRank'] += 1000; // skip;
540                                                                                                 }
541                                                                                         }
542                                                                                         else
543                                                                                         {
544                                                                                                 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
545                                                                                                 //$aSearch['iNamePhrase'] = $iPhrase;
546                                                                                         }
547                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
548                                                                                 }
549
550                                                                         }
551                                                                         elseif (($sPhraseType == '' || $sPhraseType == 'street') && $aSearchTerm['class'] == 'place' && $aSearchTerm['type'] == 'house')
552                                                                         {
553                                                                                 if ($aSearch['sHouseNumber'] === '')
554                                                                                 {
555                                                                                         $aSearch['sHouseNumber'] = $sToken;
556                                                                                         // sanity check: if the housenumber is not mainly made
557                                                                                         // up of numbers, add a penalty
558                                                                                         if (preg_match_all("/[^0-9]/", $sToken, $aMatches) > 2) $aSearch['iSearchRank']++;
559                                                                                         // also housenumbers should appear in the first or second phrase
560                                                                                         if ($iPhrase > 1) $aSearch['iSearchRank'] += 1;
561                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
562                                                                                         /*
563                                                                                         // Fall back to not searching for this item (better than nothing)
564                                                                                         $aSearch = $aCurrentSearch;
565                                                                                         $aSearch['iSearchRank'] += 1;
566                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
567                                                                                          */
568                                                                                 }
569                                                                         }
570                                                                         elseif ($sPhraseType == '' && $aSearchTerm['class'] !== '' && $aSearchTerm['class'] !== null)
571                                                                         {
572                                                                                 if ($aSearch['sClass'] === '')
573                                                                                 {
574                                                                                         $aSearch['sOperator'] = $aSearchTerm['operator'];
575                                                                                         $aSearch['sClass'] = $aSearchTerm['class'];
576                                                                                         $aSearch['sType'] = $aSearchTerm['type'];
577                                                                                         if (sizeof($aSearch['aName'])) $aSearch['sOperator'] = 'name';
578                                                                                         else $aSearch['sOperator'] = 'near'; // near = in for the moment
579                                                                                         if (strlen($aSearchTerm['operator']) == 0) $aSearch['iSearchRank'] += 1;
580
581                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
582                                                                                 }
583                                                                         }
584                                                                         elseif (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
585                                                                         {
586                                                                                 if (sizeof($aSearch['aName']))
587                                                                                 {
588                                                                                         if ((!$bStructuredPhrases || $iPhrase > 0) && $sPhraseType != 'country' && (!isset($aValidTokens[$sToken]) || strpos($sToken, ' ') !== false))
589                                                                                         {
590                                                                                                 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
591                                                                                         }
592                                                                                         else
593                                                                                         {
594                                                                                                 $aCurrentSearch['aFullNameAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
595                                                                                                 $aSearch['iSearchRank'] += 1000; // skip;
596                                                                                         }
597                                                                                 }
598                                                                                 else
599                                                                                 {
600                                                                                         $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
601                                                                                         //$aSearch['iNamePhrase'] = $iPhrase;
602                                                                                 }
603                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
604                                                                         }
605                                                                 }
606                                                         }
607                                                         // Look for partial matches.
608                                                         // Note that there is no point in adding country terms here
609                                                         // because country are omitted in the address.
610                                                         if (isset($aValidTokens[$sToken]) && $sPhraseType != 'country')
611                                                         {
612                                                                 // Allow searching for a word - but at extra cost
613                                                                 foreach($aValidTokens[$sToken] as $aSearchTerm)
614                                                                 {
615                                                                         if (isset($aSearchTerm['word_id']) && $aSearchTerm['word_id'])
616                                                                         {
617                                                                                 if ((!$bStructuredPhrases || $iPhrase > 0) && sizeof($aCurrentSearch['aName']) && strpos($sToken, ' ') === false)
618                                                                                 {
619                                                                                         $aSearch = $aCurrentSearch;
620                                                                                         $aSearch['iSearchRank'] += 1;
621                                                                                         if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
622                                                                                         {
623                                                                                                 $aSearch['aAddress'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
624                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
625                                                                                         }
626                                                                                         elseif (isset($aValidTokens[' '.$sToken])) // revert to the token version?
627                                                                                         {
628                                                                                                 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
629                                                                                                 $aSearch['iSearchRank'] += 1;
630                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
631                                                                                                 foreach($aValidTokens[' '.$sToken] as $aSearchTermToken)
632                                                                                                 {
633                                                                                                         if (empty($aSearchTermToken['country_code'])
634                                                                                                                         && empty($aSearchTermToken['lat'])
635                                                                                                                         && empty($aSearchTermToken['class']))
636                                                                                                         {
637                                                                                                                 $aSearch = $aCurrentSearch;
638                                                                                                                 $aSearch['iSearchRank'] += 1;
639                                                                                                                 $aSearch['aAddress'][$aSearchTermToken['word_id']] = $aSearchTermToken['word_id'];
640                                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
641                                                                                                         }
642                                                                                                 }
643                                                                                         }
644                                                                                         else
645                                                                                         {
646                                                                                                 $aSearch['aAddressNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
647                                                                                                 if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
648                                                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
649                                                                                         }
650                                                                                 }
651
652                                                                                 if (!sizeof($aCurrentSearch['aName']) || $aCurrentSearch['iNamePhrase'] == $iPhrase)
653                                                                                 {
654                                                                                         $aSearch = $aCurrentSearch;
655                                                                                         $aSearch['iSearchRank'] += 1;
656                                                                                         if (!sizeof($aCurrentSearch['aName'])) $aSearch['iSearchRank'] += 1;
657                                                                                         if (preg_match('#^[0-9]+$#', $sToken)) $aSearch['iSearchRank'] += 2;
658                                                                                         if ($aWordFrequencyScores[$aSearchTerm['word_id']] < CONST_Max_Word_Frequency)
659                                                                                                 $aSearch['aName'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
660                                                                                         else
661                                                                                                 $aSearch['aNameNonSearch'][$aSearchTerm['word_id']] = $aSearchTerm['word_id'];
662                                                                                         $aSearch['iNamePhrase'] = $iPhrase;
663                                                                                         if ($aSearch['iSearchRank'] < $this->iMaxRank) $aNewWordsetSearches[] = $aSearch;
664                                                                                 }
665                                                                         }
666                                                                 }
667                                                         }
668                                                         else
669                                                         {
670                                                                 // Allow skipping a word - but at EXTREAM cost
671                                                                 //$aSearch = $aCurrentSearch;
672                                                                 //$aSearch['iSearchRank']+=100;
673                                                                 //$aNewWordsetSearches[] = $aSearch;
674                                                         }
675                                                 }
676                                                 // Sort and cut
677                                                 usort($aNewWordsetSearches, 'bySearchRank');
678                                                 $aWordsetSearches = array_slice($aNewWordsetSearches, 0, 50);
679                                         }
680                                         //var_Dump('<hr>',sizeof($aWordsetSearches)); exit;
681
682                                         $aNewPhraseSearches = array_merge($aNewPhraseSearches, $aNewWordsetSearches);
683                                         usort($aNewPhraseSearches, 'bySearchRank');
684
685                                         $aSearchHash = array();
686                                         foreach($aNewPhraseSearches as $iSearch => $aSearch)
687                                         {
688                                                 $sHash = serialize($aSearch);
689                                                 if (isset($aSearchHash[$sHash])) unset($aNewPhraseSearches[$iSearch]);
690                                                 else $aSearchHash[$sHash] = 1;
691                                         }
692
693                                         $aNewPhraseSearches = array_slice($aNewPhraseSearches, 0, 50);
694                                 }
695
696                                 // Re-group the searches by their score, junk anything over 20 as just not worth trying
697                                 $aGroupedSearches = array();
698                                 foreach($aNewPhraseSearches as $aSearch)
699                                 {
700                                         if ($aSearch['iSearchRank'] < $this->iMaxRank)
701                                         {
702                                                 if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
703                                                 $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
704                                         }
705                                 }
706                                 ksort($aGroupedSearches);
707
708                                 $iSearchCount = 0;
709                                 $aSearches = array();
710                                 foreach($aGroupedSearches as $iScore => $aNewSearches)
711                                 {
712                                         $iSearchCount += sizeof($aNewSearches);
713                                         $aSearches = array_merge($aSearches, $aNewSearches);
714                                         if ($iSearchCount > 50) break;
715                                 }
716
717                                 //if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
718
719                         }
720                         return $aGroupedSearches;
721
722                 }
723
724                 /* Perform the actual query lookup.
725
726                         Returns an ordered list of results, each with the following fields:
727                           osm_type: type of corresponding OSM object
728                                                         N - node
729                                                         W - way
730                                                         R - relation
731                                                         P - postcode (internally computed)
732                           osm_id: id of corresponding OSM object
733                           class: general object class (corresponds to tag key of primary OSM tag)
734                           type: subclass of object (corresponds to tag value of primary OSM tag)
735                           admin_level: see http://wiki.openstreetmap.org/wiki/Admin_level
736                           rank_search: rank in search hierarchy
737                                                         (see also http://wiki.openstreetmap.org/wiki/Nominatim/Development_overview#Country_to_street_level)
738                           rank_address: rank in address hierarchy (determines orer in address)
739                           place_id: internal key (may differ between different instances)
740                           country_code: ISO country code
741                           langaddress: localized full address
742                           placename: localized name of object
743                           ref: content of ref tag (if available)
744                           lon: longitude
745                           lat: latitude
746                           importance: importance of place based on Wikipedia link count
747                           addressimportance: cumulated importance of address elements
748                           extra_place: type of place (for admin boundaries, if there is a place tag)
749                           aBoundingBox: bounding Box
750                           label: short description of the object class/type (English only) 
751                           name: full name (currently the same as langaddress)
752                           foundorder: secondary ordering for places with same importance
753                 */
754                 function lookup()
755                 {
756                         if (!$this->sQuery && !$this->aStructuredQuery) return false;
757
758                         $sLanguagePrefArraySQL = "ARRAY[".join(',',array_map("getDBQuoted",$this->aLangPrefOrder))."]";
759                         $sCountryCodesSQL = false;
760                         if ($this->aCountryCodes && sizeof($this->aCountryCodes))
761                         {
762                                 $sCountryCodesSQL = join(',', array_map('addQuotes', $this->aCountryCodes));
763                         }
764
765                         $sQuery = $this->sQuery;
766
767                         // Conflicts between US state abreviations and various words for 'the' in different languages
768                         if (isset($this->aLangPrefOrder['name:en']))
769                         {
770                                 $sQuery = preg_replace('/(^|,)\s*il\s*(,|$)/','\1illinois\2', $sQuery);
771                                 $sQuery = preg_replace('/(^|,)\s*al\s*(,|$)/','\1alabama\2', $sQuery);
772                                 $sQuery = preg_replace('/(^|,)\s*la\s*(,|$)/','\1louisiana\2', $sQuery);
773                         }
774
775                         // View Box SQL
776                         $sViewboxCentreSQL = false;
777                         $bBoundingBoxSearch = false;
778                         if ($this->aViewBox)
779                         {
780                                 $fHeight = $this->aViewBox[0]-$this->aViewBox[2];
781                                 $fWidth = $this->aViewBox[1]-$this->aViewBox[3];
782                                 $aBigViewBox[0] = $this->aViewBox[0] + $fHeight;
783                                 $aBigViewBox[2] = $this->aViewBox[2] - $fHeight;
784                                 $aBigViewBox[1] = $this->aViewBox[1] + $fWidth;
785                                 $aBigViewBox[3] = $this->aViewBox[3] - $fWidth;
786
787                                 $this->sViewboxSmallSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$this->aViewBox[0].",".(float)$this->aViewBox[1]."),ST_Point(".(float)$this->aViewBox[2].",".(float)$this->aViewBox[3].")),4326)";
788                                 $this->sViewboxLargeSQL = "ST_SetSRID(ST_MakeBox2D(ST_Point(".(float)$aBigViewBox[0].",".(float)$aBigViewBox[1]."),ST_Point(".(float)$aBigViewBox[2].",".(float)$aBigViewBox[3].")),4326)";
789                                 $bBoundingBoxSearch = $this->bBoundedSearch;
790                         }
791
792                         // Route SQL
793                         if ($this->aRoutePoints)
794                         {
795                                 $sViewboxCentreSQL = "ST_SetSRID('LINESTRING(";
796                                 $bFirst = true;
797                                 foreach($this->aRoutePoints as $aPoint)
798                                 {
799                                         if (!$bFirst) $sViewboxCentreSQL .= ",";
800                                         $sViewboxCentreSQL .= $aPoint[0].' '.$aPoint[1];
801                                         $bFirst = false;
802                                 }
803                                 $sViewboxCentreSQL .= ")'::geometry,4326)";
804
805                                 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/69).")";
806                                 $this->sViewboxSmallSQL = $this->oDB->getOne($sSQL);
807                                 if (PEAR::isError($this->sViewboxSmallSQL))
808                                 {
809                                         failInternalError("Could not get small viewbox.", $sSQL, $this->sViewboxSmallSQL);
810                                 }
811                                 $this->sViewboxSmallSQL = "'".$this->sViewboxSmallSQL."'::geometry";
812
813                                 $sSQL = "select st_buffer(".$sViewboxCentreSQL.",".(float)($_GET['routewidth']/30).")";
814                                 $this->sViewboxLargeSQL = $this->oDB->getOne($sSQL);
815                                 if (PEAR::isError($this->sViewboxLargeSQL))
816                                 {
817                                         failInternalError("Could not get large viewbox.", $sSQL, $this->sViewboxLargeSQL);
818                                 }
819                                 $this->sViewboxLargeSQL = "'".$this->sViewboxLargeSQL."'::geometry";
820                                 $bBoundingBoxSearch = $this->bBoundedSearch;
821                         }
822
823                         // Do we have anything that looks like a lat/lon pair?
824                         if ( $aLooksLike = looksLikeLatLonPair($sQuery) ){
825                                 $this->setNearPoint(array($aLooksLike['lat'], $aLooksLike['lon']));
826                                 $sQuery = $aLooksLike['query'];                 
827                         }
828
829                         $aSearchResults = array();
830                         if ($sQuery || $this->aStructuredQuery)
831                         {
832                                 // Start with a blank search
833                                 $aSearches = array(
834                                         array('iSearchRank' => 0, 'iNamePhrase' => -1, 'sCountryCode' => false, 'aName'=>array(), 'aAddress'=>array(), 'aFullNameAddress'=>array(),
835                                               'aNameNonSearch'=>array(), 'aAddressNonSearch'=>array(),
836                                               'sOperator'=>'', 'aFeatureName' => array(), 'sClass'=>'', 'sType'=>'', 'sHouseNumber'=>'', 'fLat'=>'', 'fLon'=>'', 'fRadius'=>'')
837                                 );
838
839                                 // Do we have a radius search?
840                                 $sNearPointSQL = false;
841                                 if ($this->aNearPoint)
842                                 {
843                                         $sNearPointSQL = "ST_SetSRID(ST_Point(".(float)$this->aNearPoint[1].",".(float)$this->aNearPoint[0]."),4326)";
844                                         $aSearches[0]['fLat'] = (float)$this->aNearPoint[0];
845                                         $aSearches[0]['fLon'] = (float)$this->aNearPoint[1];
846                                         $aSearches[0]['fRadius'] = (float)$this->aNearPoint[2];
847                                 }
848
849                                 // Any 'special' terms in the search?
850                                 $bSpecialTerms = false;
851                                 preg_match_all('/\\[(.*)=(.*)\\]/', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
852                                 $aSpecialTerms = array();
853                                 foreach($aSpecialTermsRaw as $aSpecialTerm)
854                                 {
855                                         $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
856                                         $aSpecialTerms[strtolower($aSpecialTerm[1])] = $aSpecialTerm[2];
857                                 }
858
859                                 preg_match_all('/\\[([\\w ]*)\\]/u', $sQuery, $aSpecialTermsRaw, PREG_SET_ORDER);
860                                 $aSpecialTerms = array();
861                                 if (isset($this->aStructuredQuery['amenity']) && $this->aStructuredQuery['amenity'])
862                                 {
863                                         $aSpecialTermsRaw[] = array('['.$this->aStructuredQuery['amenity'].']', $this->aStructuredQuery['amenity']);
864                                         unset($this->aStructuredQuery['amenity']);
865                                 }
866                                 foreach($aSpecialTermsRaw as $aSpecialTerm)
867                                 {
868                                         $sQuery = str_replace($aSpecialTerm[0], ' ', $sQuery);
869                                         $sToken = $this->oDB->getOne("select make_standard_name('".$aSpecialTerm[1]."') as string");
870                                         $sSQL = 'select * from (select word_id,word_token, word, class, type, country_code, operator';
871                                         $sSQL .= ' from word where word_token in (\' '.$sToken.'\')) as x where (class is not null and class not in (\'place\')) or country_code is not null';
872                                         if (CONST_Debug) var_Dump($sSQL);
873                                         $aSearchWords = $this->oDB->getAll($sSQL);
874                                         $aNewSearches = array();
875                                         foreach($aSearches as $aSearch)
876                                         {
877                                                 foreach($aSearchWords as $aSearchTerm)
878                                                 {
879                                                         $aNewSearch = $aSearch;
880                                                         if ($aSearchTerm['country_code'])
881                                                         {
882                                                                 $aNewSearch['sCountryCode'] = strtolower($aSearchTerm['country_code']);
883                                                                 $aNewSearches[] = $aNewSearch;
884                                                                 $bSpecialTerms = true;
885                                                         }
886                                                         if ($aSearchTerm['class'])
887                                                         {
888                                                                 $aNewSearch['sClass'] = $aSearchTerm['class'];
889                                                                 $aNewSearch['sType'] = $aSearchTerm['type'];
890                                                                 $aNewSearches[] = $aNewSearch;
891                                                                 $bSpecialTerms = true;
892                                                         }
893                                                 }
894                                         }
895                                         $aSearches = $aNewSearches;
896                                 }
897
898                                 // Split query into phrases
899                                 // Commas are used to reduce the search space by indicating where phrases split
900                                 if ($this->aStructuredQuery)
901                                 {
902                                         $aPhrases = $this->aStructuredQuery;
903                                         $bStructuredPhrases = true;
904                                 }
905                                 else
906                                 {
907                                         $aPhrases = explode(',',$sQuery);
908                                         $bStructuredPhrases = false;
909                                 }
910
911                                 // Convert each phrase to standard form
912                                 // Create a list of standard words
913                                 // Get all 'sets' of words
914                                 // Generate a complete list of all
915                                 $aTokens = array();
916                                 foreach($aPhrases as $iPhrase => $sPhrase)
917                                 {
918                                         $aPhrase = $this->oDB->getRow("select make_standard_name('".pg_escape_string($sPhrase)."') as string");
919                                         if (PEAR::isError($aPhrase))
920                                         {
921                                                 userError("Illegal query string (not an UTF-8 string): ".$sPhrase);
922                                                 if (CONST_Debug) var_dump($aPhrase);
923                                                 exit;
924                                         }
925                                         if (trim($aPhrase['string']))
926                                         {
927                                                 $aPhrases[$iPhrase] = $aPhrase;
928                                                 $aPhrases[$iPhrase]['words'] = explode(' ',$aPhrases[$iPhrase]['string']);
929                                                 $aPhrases[$iPhrase]['wordsets'] = getWordSets($aPhrases[$iPhrase]['words'], 0);
930                                                 $aTokens = array_merge($aTokens, getTokensFromSets($aPhrases[$iPhrase]['wordsets']));
931                                         }
932                                         else
933                                         {
934                                                 unset($aPhrases[$iPhrase]);
935                                         }
936                                 }
937
938                                 // Reindex phrases - we make assumptions later on that they are numerically keyed in order
939                                 $aPhraseTypes = array_keys($aPhrases);
940                                 $aPhrases = array_values($aPhrases);
941
942                                 if (sizeof($aTokens))
943                                 {
944                                         // Check which tokens we have, get the ID numbers
945                                         $sSQL = 'select word_id,word_token, word, class, type, country_code, operator, search_name_count';
946                                         $sSQL .= ' from word where word_token in ('.join(',',array_map("getDBQuoted",$aTokens)).')';
947
948                                         if (CONST_Debug) var_Dump($sSQL);
949
950                                         $aValidTokens = array();
951                                         if (sizeof($aTokens)) $aDatabaseWords = $this->oDB->getAll($sSQL);
952                                         else $aDatabaseWords = array();
953                                         if (PEAR::IsError($aDatabaseWords))
954                                         {
955                                                 failInternalError("Could not get word tokens.", $sSQL, $aDatabaseWords);
956                                         }
957                                         $aPossibleMainWordIDs = array();
958                                         $aWordFrequencyScores = array();
959                                         foreach($aDatabaseWords as $aToken)
960                                         {
961                                                 // Very special case - require 2 letter country param to match the country code found
962                                                 if ($bStructuredPhrases && $aToken['country_code'] && !empty($this->aStructuredQuery['country'])
963                                                                 && strlen($this->aStructuredQuery['country']) == 2 && strtolower($this->aStructuredQuery['country']) != $aToken['country_code'])
964                                                 {
965                                                         continue;
966                                                 }
967
968                                                 if (isset($aValidTokens[$aToken['word_token']]))
969                                                 {
970                                                         $aValidTokens[$aToken['word_token']][] = $aToken;
971                                                 }
972                                                 else
973                                                 {
974                                                         $aValidTokens[$aToken['word_token']] = array($aToken);
975                                                 }
976                                                 if (!$aToken['class'] && !$aToken['country_code']) $aPossibleMainWordIDs[$aToken['word_id']] = 1;
977                                                 $aWordFrequencyScores[$aToken['word_id']] = $aToken['search_name_count'] + 1;
978                                         }
979                                         if (CONST_Debug) var_Dump($aPhrases, $aValidTokens);
980
981                                         // Try and calculate GB postcodes we might be missing
982                                         foreach($aTokens as $sToken)
983                                         {
984                                                 // Source of gb postcodes is now definitive - always use
985                                                 if (preg_match('/^([A-Z][A-Z]?[0-9][0-9A-Z]? ?[0-9])([A-Z][A-Z])$/', strtoupper(trim($sToken)), $aData))
986                                                 {
987                                                         if (substr($aData[1],-2,1) != ' ')
988                                                         {
989                                                                 $aData[0] = substr($aData[0],0,strlen($aData[1])-1).' '.substr($aData[0],strlen($aData[1])-1);
990                                                                 $aData[1] = substr($aData[1],0,-1).' '.substr($aData[1],-1,1);
991                                                         }
992                                                         $aGBPostcodeLocation = gbPostcodeCalculate($aData[0], $aData[1], $aData[2], $this->oDB);
993                                                         if ($aGBPostcodeLocation)
994                                                         {
995                                                                 $aValidTokens[$sToken] = $aGBPostcodeLocation;
996                                                         }
997                                                 }
998                                                 // US ZIP+4 codes - if there is no token,
999                                                 //      merge in the 5-digit ZIP code
1000                                                 else if (!isset($aValidTokens[$sToken]) && preg_match('/^([0-9]{5}) [0-9]{4}$/', $sToken, $aData))
1001                                                 {
1002                                                         if (isset($aValidTokens[$aData[1]]))
1003                                                         {
1004                                                                 foreach($aValidTokens[$aData[1]] as $aToken)
1005                                                                 {
1006                                                                         if (!$aToken['class'])
1007                                                                         {
1008                                                                                 if (isset($aValidTokens[$sToken]))
1009                                                                                 {
1010                                                                                         $aValidTokens[$sToken][] = $aToken;
1011                                                                                 }
1012                                                                                 else
1013                                                                                 {
1014                                                                                         $aValidTokens[$sToken] = array($aToken);
1015                                                                                 }
1016                                                                         }
1017                                                                 }
1018                                                         }
1019                                                 }
1020                                         }
1021
1022                                         foreach($aTokens as $sToken)
1023                                         {
1024                                                 // Unknown single word token with a number - assume it is a house number
1025                                                 if (!isset($aValidTokens[' '.$sToken]) && strpos($sToken,' ') === false && preg_match('/[0-9]/', $sToken))
1026                                                 {
1027                                                         $aValidTokens[' '.$sToken] = array(array('class'=>'place','type'=>'house'));
1028                                                 }
1029                                         }
1030
1031                                         // Any words that have failed completely?
1032                                         // TODO: suggestions
1033
1034                                         // Start the search process
1035                                         $aResultPlaceIDs = array();
1036
1037                                         $aGroupedSearches = $this->getGroupedSearches($aSearches, $aPhraseTypes, $aPhrases, $aValidTokens, $aWordFrequencyScores, $bStructuredPhrases);
1038
1039                                         if ($this->bReverseInPlan)
1040                                         {
1041                                                 // Reverse phrase array and also reverse the order of the wordsets in
1042                                                 // the first and final phrase. Don't bother about phrases in the middle
1043                                                 // because order in the address doesn't matter.
1044                                                 $aPhrases = array_reverse($aPhrases);
1045                                                 $aPhrases[0]['wordsets'] = getInverseWordSets($aPhrases[0]['words'], 0);
1046                                                 if (sizeof($aPhrases) > 1)
1047                                                 {
1048                                                         $aFinalPhrase = end($aPhrases);
1049                                                         $aPhrases[sizeof($aPhrases)-1]['wordsets'] = getInverseWordSets($aFinalPhrase['words'], 0);
1050                                                 }
1051                                                 $aReverseGroupedSearches = $this->getGroupedSearches($aSearches, null, $aPhrases, $aValidTokens, $aWordFrequencyScores, false);
1052
1053                                                 foreach($aGroupedSearches as $aSearches)
1054                                                 {
1055                                                         foreach($aSearches as $aSearch)
1056                                                         {
1057                                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank)
1058                                                                 {
1059                                                                         if (!isset($aReverseGroupedSearches[$aSearch['iSearchRank']])) $aReverseGroupedSearches[$aSearch['iSearchRank']] = array();
1060                                                                         $aReverseGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
1061                                                                 }
1062
1063                                                         }
1064                                                 }
1065
1066                                                 $aGroupedSearches = $aReverseGroupedSearches;
1067                                                 ksort($aGroupedSearches);
1068                                         }
1069                                 }
1070                                 else
1071                                 {
1072                                         // Re-group the searches by their score, junk anything over 20 as just not worth trying
1073                                         $aGroupedSearches = array();
1074                                         foreach($aSearches as $aSearch)
1075                                         {
1076                                                 if ($aSearch['iSearchRank'] < $this->iMaxRank)
1077                                                 {
1078                                                         if (!isset($aGroupedSearches[$aSearch['iSearchRank']])) $aGroupedSearches[$aSearch['iSearchRank']] = array();
1079                                                         $aGroupedSearches[$aSearch['iSearchRank']][] = $aSearch;
1080                                                 }
1081                                         }
1082                                         ksort($aGroupedSearches);
1083                                 }
1084
1085                                 if (CONST_Debug) var_Dump($aGroupedSearches);
1086
1087                                 if (CONST_Search_TryDroppedAddressTerms && sizeof($this->aStructuredQuery) > 0)
1088                                 {
1089                                         $aCopyGroupedSearches = $aGroupedSearches;
1090                                         foreach($aCopyGroupedSearches as $iGroup => $aSearches)
1091                                         {
1092                                                 foreach($aSearches as $iSearch => $aSearch)
1093                                                 {
1094                                                         $aReductionsList = array($aSearch['aAddress']);
1095                                                         $iSearchRank = $aSearch['iSearchRank'];
1096                                                         while(sizeof($aReductionsList) > 0)
1097                                                         {
1098                                                                 $iSearchRank += 5;
1099                                                                 if ($iSearchRank > iMaxRank) break 3;
1100                                                                 $aNewReductionsList = array();
1101                                                                 foreach($aReductionsList as $aReductionsWordList)
1102                                                                 {
1103                                                                         for ($iReductionWord = 0; $iReductionWord < sizeof($aReductionsWordList); $iReductionWord++)
1104                                                                         {
1105                                                                                 $aReductionsWordListResult = array_merge(array_slice($aReductionsWordList, 0, $iReductionWord), array_slice($aReductionsWordList, $iReductionWord+1));
1106                                                                                 $aReverseSearch = $aSearch;
1107                                                                                 $aSearch['aAddress'] = $aReductionsWordListResult;
1108                                                                                 $aSearch['iSearchRank'] = $iSearchRank;
1109                                                                                 $aGroupedSearches[$iSearchRank][] = $aReverseSearch;
1110                                                                                 if (sizeof($aReductionsWordListResult) > 0)
1111                                                                                 {
1112                                                                                         $aNewReductionsList[] = $aReductionsWordListResult;
1113                                                                                 }
1114                                                                         }
1115                                                                 }
1116                                                                 $aReductionsList = $aNewReductionsList;
1117                                                         }
1118                                                 }
1119                                         }
1120                                         ksort($aGroupedSearches);
1121                                 }
1122
1123                                 // Filter out duplicate searches
1124                                 $aSearchHash = array();
1125                                 foreach($aGroupedSearches as $iGroup => $aSearches)
1126                                 {
1127                                         foreach($aSearches as $iSearch => $aSearch)
1128                                         {
1129                                                 $sHash = serialize($aSearch);
1130                                                 if (isset($aSearchHash[$sHash]))
1131                                                 {
1132                                                         unset($aGroupedSearches[$iGroup][$iSearch]);
1133                                                         if (sizeof($aGroupedSearches[$iGroup]) == 0) unset($aGroupedSearches[$iGroup]);
1134                                                 }
1135                                                 else
1136                                                 {
1137                                                         $aSearchHash[$sHash] = 1;
1138                                                 }
1139                                         }
1140                                 }
1141
1142                                 if (CONST_Debug) _debugDumpGroupedSearches($aGroupedSearches, $aValidTokens);
1143
1144                                 $iGroupLoop = 0;
1145                                 $iQueryLoop = 0;
1146                                 foreach($aGroupedSearches as $iGroupedRank => $aSearches)
1147                                 {
1148                                         $iGroupLoop++;
1149                                         foreach($aSearches as $aSearch)
1150                                         {
1151                                                 $iQueryLoop++;
1152
1153                                                 if (CONST_Debug) { echo "<hr><b>Search Loop, group $iGroupLoop, loop $iQueryLoop</b>"; }
1154                                                 if (CONST_Debug) _debugDumpGroupedSearches(array($iGroupedRank => array($aSearch)), $aValidTokens);
1155
1156                                                 // No location term?
1157                                                 if (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && !$aSearch['fLon'])
1158                                                 {
1159                                                         if ($aSearch['sCountryCode'] && !$aSearch['sClass'] && !$aSearch['sHouseNumber'])
1160                                                         {
1161                                                                 // Just looking for a country by code - look it up
1162                                                                 if (4 >= $this->iMinAddressRank && 4 <= $this->iMaxAddressRank)
1163                                                                 {
1164                                                                         $sSQL = "select place_id from placex where calculated_country_code='".$aSearch['sCountryCode']."' and rank_search = 4";
1165                                                                         if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1166                                     if ($bBoundingBoxSearch)
1167                                         $sSQL .= " and _st_intersects($this->sViewboxSmallSQL, geometry)";
1168                                                                         $sSQL .= " order by st_area(geometry) desc limit 1";
1169                                                                         if (CONST_Debug) var_dump($sSQL);
1170                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1171                                                                 }
1172                                                                 else
1173                                                                 {
1174                                                                         $aPlaceIDs = array();
1175                                                                 }
1176                                                         }
1177                                                         else
1178                                                         {
1179                                                                 if (!$bBoundingBoxSearch && !$aSearch['fLon']) continue;
1180                                                                 if (!$aSearch['sClass']) continue;
1181                                                                 $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1182                                                                 if ($this->oDB->getOne($sSQL))
1183                                                                 {
1184                                                                         $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1185                                                                         if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1186                                                                         $sSQL .= " where st_contains($this->sViewboxSmallSQL, ct.centroid)";
1187                                                                         if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1188                                                                         if (sizeof($this->aExcludePlaceIDs))
1189                                                                         {
1190                                                                                 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1191                                                                         }
1192                                                                         if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1193                                                                         $sSQL .= " limit $this->iLimit";
1194                                                                         if (CONST_Debug) var_dump($sSQL);
1195                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1196
1197                                                                         // If excluded place IDs are given, it is fair to assume that
1198                                                                         // there have been results in the small box, so no further
1199                                                                         // expansion in that case.
1200                                                                         // Also don't expand if bounded results were requested.
1201                                                                         if (!sizeof($aPlaceIDs) && !sizeof($this->aExcludePlaceIDs) && !$this->bBoundedSearch)
1202                                                                         {
1203                                                                                 $sSQL = "select place_id from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." ct";
1204                                                                                 if ($sCountryCodesSQL) $sSQL .= " join placex using (place_id)";
1205                                                                                 $sSQL .= " where st_contains($this->sViewboxLargeSQL, ct.centroid)";
1206                                                                                 if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1207                                                                                 if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, ct.centroid) asc";
1208                                                                                 $sSQL .= " limit $this->iLimit";
1209                                                                                 if (CONST_Debug) var_dump($sSQL);
1210                                                                                 $aPlaceIDs = $this->oDB->getCol($sSQL);
1211                                                                         }
1212                                                                 }
1213                                                                 else
1214                                                                 {
1215                                                                         $sSQL = "select place_id from placex where class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1216                                                                         $sSQL .= " and st_contains($this->sViewboxSmallSQL, geometry) and linked_place_id is null";
1217                                                                         if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1218                                                                         if ($sViewboxCentreSQL) $sSQL .= " order by st_distance($sViewboxCentreSQL, centroid) asc";
1219                                                                         $sSQL .= " limit $this->iLimit";
1220                                                                         if (CONST_Debug) var_dump($sSQL);
1221                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1222                                                                 }
1223                                                         }
1224                                                 }
1225                                                 else
1226                                                 {
1227                                                         $aPlaceIDs = array();
1228
1229                                                         // First we need a position, either aName or fLat or both
1230                                                         $aTerms = array();
1231                                                         $aOrder = array();
1232
1233                                                         if ($aSearch['sHouseNumber'] && sizeof($aSearch['aAddress']))
1234                                                         {
1235                                                                 $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M';
1236                                                                 $aOrder[] = "exists(select place_id from placex where parent_place_id = search_name.place_id and transliteration(housenumber) ~* E'".$sHouseNumberRegex."' limit 1) desc";
1237                                                         }
1238
1239                                                         // TODO: filter out the pointless search terms (2 letter name tokens and less)
1240                                                         // they might be right - but they are just too darned expensive to run
1241                                                         if (sizeof($aSearch['aName'])) $aTerms[] = "name_vector @> ARRAY[".join($aSearch['aName'],",")."]";
1242                                                         if (sizeof($aSearch['aNameNonSearch'])) $aTerms[] = "array_cat(name_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aNameNonSearch'],",")."]";
1243                                                         if (sizeof($aSearch['aAddress']) && $aSearch['aName'] != $aSearch['aAddress'])
1244                                                         {
1245                                                                 // For infrequent name terms disable index usage for address
1246                                                                 if (CONST_Search_NameOnlySearchFrequencyThreshold &&
1247                                                                                 sizeof($aSearch['aName']) == 1 &&
1248                                                                                 $aWordFrequencyScores[$aSearch['aName'][reset($aSearch['aName'])]] < CONST_Search_NameOnlySearchFrequencyThreshold)
1249                                                                 {
1250                                                                         $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join(array_merge($aSearch['aAddress'],$aSearch['aAddressNonSearch']),",")."]";
1251                                                                 }
1252                                                                 else
1253                                                                 {
1254                                                                         $aTerms[] = "nameaddress_vector @> ARRAY[".join($aSearch['aAddress'],",")."]";
1255                                                                         if (sizeof($aSearch['aAddressNonSearch'])) $aTerms[] = "array_cat(nameaddress_vector,ARRAY[]::integer[]) @> ARRAY[".join($aSearch['aAddressNonSearch'],",")."]";
1256                                                                 }
1257                                                         }
1258                                                         if ($aSearch['sCountryCode']) $aTerms[] = "country_code = '".pg_escape_string($aSearch['sCountryCode'])."'";
1259                                                         if ($aSearch['sHouseNumber'])
1260                                                         {
1261                                                                 $aTerms[] = "address_rank between 16 and 27";
1262                                                         }
1263                                                         else
1264                                                         {
1265                                                                 if ($this->iMinAddressRank > 0)
1266                                                                 {
1267                                                                         $aTerms[] = "address_rank >= ".$this->iMinAddressRank;
1268                                                                 }
1269                                                                 if ($this->iMaxAddressRank < 30)
1270                                                                 {
1271                                                                         $aTerms[] = "address_rank <= ".$this->iMaxAddressRank;
1272                                                                 }
1273                                                         }
1274                                                         if ($aSearch['fLon'] && $aSearch['fLat'])
1275                                                         {
1276                                                                 $aTerms[] = "ST_DWithin(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326), ".$aSearch['fRadius'].")";
1277                                                                 $aOrder[] = "ST_Distance(centroid, ST_SetSRID(ST_Point(".$aSearch['fLon'].",".$aSearch['fLat']."),4326)) ASC";
1278                                                         }
1279                                                         if (sizeof($this->aExcludePlaceIDs))
1280                                                         {
1281                                                                 $aTerms[] = "place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1282                                                         }
1283                                                         if ($sCountryCodesSQL)
1284                                                         {
1285                                                                 $aTerms[] = "country_code in ($sCountryCodesSQL)";
1286                                                         }
1287
1288                                                         if ($bBoundingBoxSearch) $aTerms[] = "centroid && $this->sViewboxSmallSQL";
1289                                                         if ($sNearPointSQL) $aOrder[] = "ST_Distance($sNearPointSQL, centroid) asc";
1290
1291                                                         if ($aSearch['sHouseNumber'])
1292                                                         {
1293                                                                 $sImportanceSQL = '- abs(26 - address_rank) + 3';
1294                                                         }
1295                                                         else
1296                                                         {
1297                                                                 $sImportanceSQL = '(case when importance = 0 OR importance IS NULL then 0.75-(search_rank::float/40) else importance end)';
1298                                                         }
1299                                                         if ($this->sViewboxSmallSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxSmallSQL, centroid) THEN 1 ELSE 0.5 END";
1300                                                         if ($this->sViewboxLargeSQL) $sImportanceSQL .= " * case when ST_Contains($this->sViewboxLargeSQL, centroid) THEN 1 ELSE 0.5 END";
1301
1302                                                         $aOrder[] = "$sImportanceSQL DESC";
1303                                                         if (sizeof($aSearch['aFullNameAddress']))
1304                                                         {
1305                                                                 $sExactMatchSQL = '(select count(*) from (select unnest(ARRAY['.join($aSearch['aFullNameAddress'],",").']) INTERSECT select unnest(nameaddress_vector))s) as exactmatch';
1306                                                                 $aOrder[] = 'exactmatch DESC';
1307                                                         } else {
1308                                                                 $sExactMatchSQL = '0::int as exactmatch';
1309                                                         }
1310
1311                                                         if (sizeof($aTerms))
1312                                                         {
1313                                                                 $sSQL = "select place_id, ";
1314                                                                 $sSQL .= $sExactMatchSQL;
1315                                                                 $sSQL .= " from search_name";
1316                                                                 $sSQL .= " where ".join(' and ',$aTerms);
1317                                                                 $sSQL .= " order by ".join(', ',$aOrder);
1318                                                                 if ($aSearch['sHouseNumber'] || $aSearch['sClass'])
1319                                                                         $sSQL .= " limit 20";
1320                                                                 elseif (!sizeof($aSearch['aName']) && !sizeof($aSearch['aAddress']) && $aSearch['sClass'])
1321                                                                         $sSQL .= " limit 1";
1322                                                                 else
1323                                                                         $sSQL .= " limit ".$this->iLimit;
1324
1325                                                                 if (CONST_Debug) { var_dump($sSQL); }
1326                                                                 $aViewBoxPlaceIDs = $this->oDB->getAll($sSQL);
1327                                                                 if (PEAR::IsError($aViewBoxPlaceIDs))
1328                                                                 {
1329                                                                         failInternalError("Could not get places for search terms.", $sSQL, $aViewBoxPlaceIDs);
1330                                                                 }
1331                                                                 //var_dump($aViewBoxPlaceIDs);
1332                                                                 // Did we have an viewbox matches?
1333                                                                 $aPlaceIDs = array();
1334                                                                 $bViewBoxMatch = false;
1335                                                                 foreach($aViewBoxPlaceIDs as $aViewBoxRow)
1336                                                                 {
1337                                                                         //if ($bViewBoxMatch == 1 && $aViewBoxRow['in_small'] == 'f') break;
1338                                                                         //if ($bViewBoxMatch == 2 && $aViewBoxRow['in_large'] == 'f') break;
1339                                                                         //if ($aViewBoxRow['in_small'] == 't') $bViewBoxMatch = 1;
1340                                                                         //else if ($aViewBoxRow['in_large'] == 't') $bViewBoxMatch = 2;
1341                                                                         $aPlaceIDs[] = $aViewBoxRow['place_id'];
1342                                                                         $this->exactMatchCache[$aViewBoxRow['place_id']] = $aViewBoxRow['exactmatch'];
1343                                                                 }
1344                                                         }
1345                                                         //var_Dump($aPlaceIDs);
1346                                                         //exit;
1347
1348                                                         if ($aSearch['sHouseNumber'] && sizeof($aPlaceIDs))
1349                                                         {
1350                                                                 $aRoadPlaceIDs = $aPlaceIDs;
1351                                                                 $sPlaceIDs = join(',',$aPlaceIDs);
1352
1353                                                                 // Now they are indexed look for a house attached to a street we found
1354                                                                 $sHouseNumberRegex = '\\\\m'.$aSearch['sHouseNumber'].'\\\\M';
1355                                                                 $sSQL = "select place_id from placex where parent_place_id in (".$sPlaceIDs.") and transliteration(housenumber) ~* E'".$sHouseNumberRegex."'";
1356                                                                 if (sizeof($this->aExcludePlaceIDs))
1357                                                                 {
1358                                                                         $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1359                                                                 }
1360                                                                 $sSQL .= " limit $this->iLimit";
1361                                                                 if (CONST_Debug) var_dump($sSQL);
1362                                                                 $aPlaceIDs = $this->oDB->getCol($sSQL);
1363
1364                                                                 // If not try the aux fallback table
1365                                                                 if (!sizeof($aPlaceIDs))
1366                                                                 {
1367                                                                         $sSQL = "select place_id from location_property_aux where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1368                                                                         if (sizeof($this->aExcludePlaceIDs))
1369                                                                         {
1370                                                                                 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1371                                                                         }
1372                                                                         //$sSQL .= " limit $this->iLimit";
1373                                                                         if (CONST_Debug) var_dump($sSQL);
1374                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1375                                                                 }
1376
1377                                                                 if (!sizeof($aPlaceIDs))
1378                                                                 {
1379                                                                         $sSQL = "select place_id from location_property_tiger where parent_place_id in (".$sPlaceIDs.") and housenumber = '".pg_escape_string($aSearch['sHouseNumber'])."'";
1380                                                                         if (sizeof($this->aExcludePlaceIDs))
1381                                                                         {
1382                                                                                 $sSQL .= " and place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1383                                                                         }
1384                                                                         //$sSQL .= " limit $this->iLimit";
1385                                                                         if (CONST_Debug) var_dump($sSQL);
1386                                                                         $aPlaceIDs = $this->oDB->getCol($sSQL);
1387                                                                 }
1388
1389                                                                 // Fallback to the road
1390                                                                 if (!sizeof($aPlaceIDs) && preg_match('/[0-9]+/', $aSearch['sHouseNumber']))
1391                                                                 {
1392                                                                         $aPlaceIDs = $aRoadPlaceIDs;
1393                                                                 }
1394
1395                                                         }
1396
1397                                                         if ($aSearch['sClass'] && sizeof($aPlaceIDs))
1398                                                         {
1399                                                                 $sPlaceIDs = join(',',$aPlaceIDs);
1400                                                                 $aClassPlaceIDs = array();
1401
1402                                                                 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'name')
1403                                                                 {
1404                                                                         // If they were searching for a named class (i.e. 'Kings Head pub') then we might have an extra match
1405                                                                         $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and class='".$aSearch['sClass']."' and type='".$aSearch['sType']."'";
1406                                                                         $sSQL .= " and linked_place_id is null";
1407                                                                         if ($sCountryCodesSQL) $sSQL .= " and calculated_country_code in ($sCountryCodesSQL)";
1408                                                                         $sSQL .= " order by rank_search asc limit $this->iLimit";
1409                                                                         if (CONST_Debug) var_dump($sSQL);
1410                                                                         $aClassPlaceIDs = $this->oDB->getCol($sSQL);
1411                                                                 }
1412
1413                                                                 if (!$aSearch['sOperator'] || $aSearch['sOperator'] == 'near') // & in
1414                                                                 {
1415                                                                         $sSQL = "select count(*) from pg_tables where tablename = 'place_classtype_".$aSearch['sClass']."_".$aSearch['sType']."'";
1416                                                                         $bCacheTable = $this->oDB->getOne($sSQL);
1417
1418                                                                         $sSQL = "select min(rank_search) from placex where place_id in ($sPlaceIDs)";
1419
1420                                                                         if (CONST_Debug) var_dump($sSQL);
1421                                                                         $this->iMaxRank = ((int)$this->oDB->getOne($sSQL));
1422
1423                                                                         // For state / country level searches the normal radius search doesn't work very well
1424                                                                         $sPlaceGeom = false;
1425                                                                         if ($this->iMaxRank < 9 && $bCacheTable)
1426                                                                         {
1427                                                                                 // Try and get a polygon to search in instead
1428                                                                                 $sSQL = "select geometry from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank + 5 and st_geometrytype(geometry) in ('ST_Polygon','ST_MultiPolygon') order by rank_search asc limit 1";
1429                                                                                 if (CONST_Debug) var_dump($sSQL);
1430                                                                                 $sPlaceGeom = $this->oDB->getOne($sSQL);
1431                                                                         }
1432
1433                                                                         if ($sPlaceGeom)
1434                                                                         {
1435                                                                                 $sPlaceIDs = false;
1436                                                                         }
1437                                                                         else
1438                                                                         {
1439                                                                                 $this->iMaxRank += 5;
1440                                                                                 $sSQL = "select place_id from placex where place_id in ($sPlaceIDs) and rank_search < $this->iMaxRank";
1441                                                                                 if (CONST_Debug) var_dump($sSQL);
1442                                                                                 $aPlaceIDs = $this->oDB->getCol($sSQL);
1443                                                                                 $sPlaceIDs = join(',',$aPlaceIDs);
1444                                                                         }
1445
1446                                                                         if ($sPlaceIDs || $sPlaceGeom)
1447                                                                         {
1448
1449                                                                                 $fRange = 0.01;
1450                                                                                 if ($bCacheTable)
1451                                                                                 {
1452                                                                                         // More efficient - can make the range bigger
1453                                                                                         $fRange = 0.05;
1454
1455                                                                                         $sOrderBySQL = '';
1456                                                                                         if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.centroid)";
1457                                                                                         else if ($sPlaceIDs) $sOrderBySQL = "ST_Distance(l.centroid, f.geometry)";
1458                                                                                         else if ($sPlaceGeom) $sOrderBysSQL = "ST_Distance(st_centroid('".$sPlaceGeom."'), l.centroid)";
1459
1460                                                                                         $sSQL = "select distinct l.place_id".($sOrderBySQL?','.$sOrderBySQL:'')." from place_classtype_".$aSearch['sClass']."_".$aSearch['sType']." as l";
1461                                                                                         if ($sCountryCodesSQL) $sSQL .= " join placex as lp using (place_id)";
1462                                                                                         if ($sPlaceIDs)
1463                                                                                         {
1464                                                                                                 $sSQL .= ",placex as f where ";
1465                                                                                                 $sSQL .= "f.place_id in ($sPlaceIDs) and ST_DWithin(l.centroid, f.centroid, $fRange) ";
1466                                                                                         }
1467                                                                                         if ($sPlaceGeom)
1468                                                                                         {
1469                                                                                                 $sSQL .= " where ";
1470                                                                                                 $sSQL .= "ST_Contains('".$sPlaceGeom."', l.centroid) ";
1471                                                                                         }
1472                                                                                         if (sizeof($this->aExcludePlaceIDs))
1473                                                                                         {
1474                                                                                                 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1475                                                                                         }
1476                                                                                         if ($sCountryCodesSQL) $sSQL .= " and lp.calculated_country_code in ($sCountryCodesSQL)";
1477                                                                                         if ($sOrderBySQL) $sSQL .= "order by ".$sOrderBySQL." asc";
1478                                                                                         if ($this->iOffset) $sSQL .= " offset $this->iOffset";
1479                                                                                         $sSQL .= " limit $this->iLimit";
1480                                                                                         if (CONST_Debug) var_dump($sSQL);
1481                                                                                         $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1482                                                                                 }
1483                                                                                 else
1484                                                                                 {
1485                                                                                         if (isset($aSearch['fRadius']) && $aSearch['fRadius']) $fRange = $aSearch['fRadius'];
1486
1487                                                                                         $sOrderBySQL = '';
1488                                                                                         if ($sNearPointSQL) $sOrderBySQL = "ST_Distance($sNearPointSQL, l.geometry)";
1489                                                                                         else $sOrderBySQL = "ST_Distance(l.geometry, f.geometry)";
1490
1491                                                                                         $sSQL = "select distinct l.place_id".($sOrderBysSQL?','.$sOrderBysSQL:'')." from placex as l,placex as f where ";
1492                                                                                         $sSQL .= "f.place_id in ( $sPlaceIDs) and ST_DWithin(l.geometry, f.centroid, $fRange) ";
1493                                                                                         $sSQL .= "and l.class='".$aSearch['sClass']."' and l.type='".$aSearch['sType']."' ";
1494                                                                                         if (sizeof($this->aExcludePlaceIDs))
1495                                                                                         {
1496                                                                                                 $sSQL .= " and l.place_id not in (".join(',',$this->aExcludePlaceIDs).")";
1497                                                                                         }
1498                                                                                         if ($sCountryCodesSQL) $sSQL .= " and l.calculated_country_code in ($sCountryCodesSQL)";
1499                                                                                         if ($sOrderBy) $sSQL .= "order by ".$OrderBysSQL." asc";
1500                                                                                         if ($this->iOffset) $sSQL .= " offset $this->iOffset";
1501                                                                                         $sSQL .= " limit $this->iLimit";
1502                                                                                         if (CONST_Debug) var_dump($sSQL);
1503                                                                                         $aClassPlaceIDs = array_merge($aClassPlaceIDs, $this->oDB->getCol($sSQL));
1504                                                                                 }
1505                                                                         }
1506                                                                 }
1507
1508                                                                 $aPlaceIDs = $aClassPlaceIDs;
1509
1510                                                         }
1511
1512                                                 }
1513
1514                                                 if (PEAR::IsError($aPlaceIDs))
1515                                                 {
1516                                                         failInternalError("Could not get place IDs from tokens." ,$sSQL, $aPlaceIDs);
1517                                                 }
1518
1519                                                 if (CONST_Debug) { echo "<br><b>Place IDs:</b> "; var_Dump($aPlaceIDs); }
1520
1521                                                 foreach($aPlaceIDs as $iPlaceID)
1522                                                 {
1523                                                         $aResultPlaceIDs[$iPlaceID] = $iPlaceID;
1524                                                 }
1525                                                 if ($iQueryLoop > 20) break;
1526                                         }
1527
1528                                         if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs) && ($this->iMinAddressRank != 0 || $this->iMaxAddressRank != 30))
1529                                         {
1530                                                 // Need to verify passes rank limits before dropping out of the loop (yuk!)
1531                                                 $sSQL = "select place_id from placex where place_id in (".join(',',$aResultPlaceIDs).") ";
1532                                                 $sSQL .= "and (placex.rank_address between $this->iMinAddressRank and $this->iMaxAddressRank ";
1533                                                 if (14 >= $this->iMinAddressRank && 14 <= $this->iMaxAddressRank) $sSQL .= " OR (extratags->'place') = 'city'";
1534                                                 if ($this->aAddressRankList) $sSQL .= " OR placex.rank_address in (".join(',',$this->aAddressRankList).")";
1535                                                 $sSQL .= ") UNION select place_id from location_property_tiger where place_id in (".join(',',$aResultPlaceIDs).") ";
1536                                                 $sSQL .= "and (30 between $this->iMinAddressRank and $this->iMaxAddressRank ";
1537                                                 if ($this->aAddressRankList) $sSQL .= " OR 30 in (".join(',',$this->aAddressRankList).")";
1538                                                 $sSQL .= ")";
1539                                                 if (CONST_Debug) var_dump($sSQL);
1540                                                 $aResultPlaceIDs = $this->oDB->getCol($sSQL);
1541                                         }
1542
1543                                         //exit;
1544                                         if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs)) break;
1545                                         if ($iGroupLoop > 4) break;
1546                                         if ($iQueryLoop > 30) break;
1547                                 }
1548
1549                                 // Did we find anything?
1550                                 if (isset($aResultPlaceIDs) && sizeof($aResultPlaceIDs))
1551                                 {
1552                                         $aSearchResults = $this->getDetails($aResultPlaceIDs);
1553                                 }
1554
1555                         }
1556                         else
1557                         {
1558                                 // Just interpret as a reverse geocode
1559                                 $iPlaceID = geocodeReverse((float)$this->aNearPoint[0], (float)$this->aNearPoint[1]);
1560                                 if ($iPlaceID)
1561                                         $aSearchResults = $this->getDetails(array($iPlaceID));
1562                                 else
1563                                         $aSearchResults = array();
1564                         }
1565
1566                         // No results? Done
1567                         if (!sizeof($aSearchResults))
1568                         {
1569                                 if ($this->bFallback)
1570                                 {
1571                                         if ($this->fallbackStructuredQuery())
1572                                         {
1573                                                 return $this->lookup();
1574                                         }
1575                                 }
1576
1577                                 return array();
1578                         }
1579
1580                         $aClassType = getClassTypesWithImportance();
1581                         $aRecheckWords = preg_split('/\b[\s,\\-]*/u',$sQuery);
1582                         foreach($aRecheckWords as $i => $sWord)
1583                         {
1584                                 if (!preg_match('/\pL/', $sWord)) unset($aRecheckWords[$i]);
1585                         }
1586
1587             if (CONST_Debug) { echo '<i>Recheck words:<\i>'; var_dump($aRecheckWords); }
1588
1589                         foreach($aSearchResults as $iResNum => $aResult)
1590                         {
1591                                 if (CONST_Search_AreaPolygons)
1592                                 {
1593                                         // Get the bounding box and outline polygon
1594                                         $sSQL = "select place_id,0 as numfeatures,st_area(geometry) as area,";
1595                                         $sSQL .= "ST_Y(centroid) as centrelat,ST_X(centroid) as centrelon,";
1596                                         $sSQL .= "ST_YMin(geometry) as minlat,ST_YMax(geometry) as maxlat,";
1597                                         $sSQL .= "ST_XMin(geometry) as minlon,ST_XMax(geometry) as maxlon";
1598                                         if ($this->bIncludePolygonAsGeoJSON) $sSQL .= ",ST_AsGeoJSON(geometry) as asgeojson";
1599                                         if ($this->bIncludePolygonAsKML) $sSQL .= ",ST_AsKML(geometry) as askml";
1600                                         if ($this->bIncludePolygonAsSVG) $sSQL .= ",ST_AsSVG(geometry) as assvg";
1601                                         if ($this->bIncludePolygonAsText || $this->bIncludePolygonAsPoints) $sSQL .= ",ST_AsText(geometry) as astext";
1602                                         $sSQL .= " from placex where place_id = ".$aResult['place_id'];
1603                                         $aPointPolygon = $this->oDB->getRow($sSQL);
1604                                         if (PEAR::IsError($aPointPolygon))
1605                                         {
1606                                                 failInternalError("Could not get outline.", $sSQL, $aPointPolygon);
1607                                         }
1608
1609                                         if ($aPointPolygon['place_id'])
1610                                         {
1611                                                 if ($this->bIncludePolygonAsGeoJSON) $aResult['asgeojson'] = $aPointPolygon['asgeojson'];
1612                                                 if ($this->bIncludePolygonAsKML) $aResult['askml'] = $aPointPolygon['askml'];
1613                                                 if ($this->bIncludePolygonAsSVG) $aResult['assvg'] = $aPointPolygon['assvg'];
1614                                                 if ($this->bIncludePolygonAsText) $aResult['astext'] = $aPointPolygon['astext'];
1615
1616                                                 if ($aPointPolygon['centrelon'] !== null && $aPointPolygon['centrelat'] !== null )
1617                                                 {
1618                                                         $aResult['lat'] = $aPointPolygon['centrelat'];
1619                                                         $aResult['lon'] = $aPointPolygon['centrelon'];
1620                                                 }
1621
1622                                                 if ($this->bIncludePolygonAsPoints)
1623                                                 {
1624                                                         // Translate geometry string to point array
1625                                                         if (preg_match('#POLYGON\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1626                                                         {
1627                                                                 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1628                                                         }
1629                                                         elseif (preg_match('#MULTIPOLYGON\\(\\(\\(([- 0-9.,]+)#',$aPointPolygon['astext'],$aMatch))
1630                                                         {
1631                                                                 preg_match_all('/(-?[0-9.]+) (-?[0-9.]+)/',$aMatch[1],$aPolyPoints,PREG_SET_ORDER);
1632                                                         }
1633                                                         elseif (preg_match('#POINT\\((-?[0-9.]+) (-?[0-9.]+)\\)#',$aPointPolygon['astext'],$aMatch))
1634                                                         {
1635                                                                 $fRadius = 0.01;
1636                                                                 $iSteps = ($fRadius * 40000)^2;
1637                                                                 $fStepSize = (2*pi())/$iSteps;
1638                                                                 $aPolyPoints = array();
1639                                                                 for($f = 0; $f < 2*pi(); $f += $fStepSize)
1640                                                                 {
1641                                                                         $aPolyPoints[] = array('',$aMatch[1]+($fRadius*sin($f)),$aMatch[2]+($fRadius*cos($f)));
1642                                                                 }
1643                                                                 $aPointPolygon['minlat'] = $aPointPolygon['minlat'] - $fRadius;
1644                                                                 $aPointPolygon['maxlat'] = $aPointPolygon['maxlat'] + $fRadius;
1645                                                                 $aPointPolygon['minlon'] = $aPointPolygon['minlon'] - $fRadius;
1646                                                                 $aPointPolygon['maxlon'] = $aPointPolygon['maxlon'] + $fRadius;
1647                                                         }
1648                                                 }
1649
1650                                                 // Output data suitable for display (points and a bounding box)
1651                                                 if ($this->bIncludePolygonAsPoints && isset($aPolyPoints))
1652                                                 {
1653                                                         $aResult['aPolyPoints'] = array();
1654                                                         foreach($aPolyPoints as $aPoint)
1655                                                         {
1656                                                                 $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1657                                                         }
1658                                                 }
1659                                                 $aResult['aBoundingBox'] = array($aPointPolygon['minlat'],$aPointPolygon['maxlat'],$aPointPolygon['minlon'],$aPointPolygon['maxlon']);
1660                                         }
1661                                 }
1662
1663                                 if ($aResult['extra_place'] == 'city')
1664                                 {
1665                                         $aResult['class'] = 'place';
1666                                         $aResult['type'] = 'city';
1667                                         $aResult['rank_search'] = 16;
1668                                 }
1669
1670                                 if (!isset($aResult['aBoundingBox']))
1671                                 {
1672                                         // Default
1673                                         $fDiameter = 0.0001;
1674
1675                                         if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1676                                                         && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defdiameter'])
1677                                         {
1678                                                 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['defzoom'];
1679                                         }
1680                                         elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1681                                                         && $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'])
1682                                         {
1683                                                 $fDiameter = $aClassType[$aResult['class'].':'.$aResult['type']]['defdiameter'];
1684                                         }
1685                                         $fRadius = $fDiameter / 2;
1686
1687                                         $iSteps = max(8,min(100,$fRadius * 3.14 * 100000));
1688                                         $fStepSize = (2*pi())/$iSteps;
1689                                         $aPolyPoints = array();
1690                                         for($f = 0; $f < 2*pi(); $f += $fStepSize)
1691                                         {
1692                                                 $aPolyPoints[] = array('',$aResult['lon']+($fRadius*sin($f)),$aResult['lat']+($fRadius*cos($f)));
1693                                         }
1694                                         $aPointPolygon['minlat'] = $aResult['lat'] - $fRadius;
1695                                         $aPointPolygon['maxlat'] = $aResult['lat'] + $fRadius;
1696                                         $aPointPolygon['minlon'] = $aResult['lon'] - $fRadius;
1697                                         $aPointPolygon['maxlon'] = $aResult['lon'] + $fRadius;
1698
1699                                         // Output data suitable for display (points and a bounding box)
1700                                         if ($this->bIncludePolygonAsPoints)
1701                                         {
1702                                                 $aResult['aPolyPoints'] = array();
1703                                                 foreach($aPolyPoints as $aPoint)
1704                                                 {
1705                                                         $aResult['aPolyPoints'][] = array($aPoint[1], $aPoint[2]);
1706                                                 }
1707                                         }
1708                                         $aResult['aBoundingBox'] = array((string)$aPointPolygon['minlat'],(string)$aPointPolygon['maxlat'],(string)$aPointPolygon['minlon'],(string)$aPointPolygon['maxlon']);
1709                                 }
1710
1711                                 // Is there an icon set for this type of result?
1712                                 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1713                                                 && $aClassType[$aResult['class'].':'.$aResult['type']]['icon'])
1714                                 {
1715                                         $aResult['icon'] = CONST_Website_BaseURL.'images/mapicons/'.$aClassType[$aResult['class'].':'.$aResult['type']]['icon'].'.p.20.png';
1716                                 }
1717
1718                                 if (isset($aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1719                                                 && $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'])
1720                                 {
1721                                         $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type'].':'.$aResult['admin_level']]['label'];
1722                                 }
1723                                 elseif (isset($aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1724                                                 && $aClassType[$aResult['class'].':'.$aResult['type']]['label'])
1725                                 {
1726                                         $aResult['label'] = $aClassType[$aResult['class'].':'.$aResult['type']]['label'];
1727                                 }
1728
1729                                 if ($this->bIncludeAddressDetails)
1730                                 {
1731                                         $aResult['address'] = getAddressDetails($this->oDB, $sLanguagePrefArraySQL, $aResult['place_id'], $aResult['country_code']);
1732                                         if ($aResult['extra_place'] == 'city' && !isset($aResult['address']['city']))
1733                                         {
1734                                                 $aResult['address'] = array_merge(array('city' => array_shift(array_values($aResult['address']))), $aResult['address']);
1735                                         }
1736                                 }
1737
1738                                 // Adjust importance for the number of exact string matches in the result
1739                                 $aResult['importance'] = max(0.001,$aResult['importance']);
1740                                 $iCountWords = 0;
1741                                 $sAddress = $aResult['langaddress'];
1742                                 foreach($aRecheckWords as $i => $sWord)
1743                                 {
1744                                         if (stripos($sAddress, $sWord)!==false)
1745                                         {
1746                                                 $iCountWords++;
1747                                                 if (preg_match("/(^|,)\s*".preg_quote($sWord, '/')."\s*(,|$)/", $sAddress)) $iCountWords += 0.1;
1748                                         }
1749                                 }
1750
1751                                 $aResult['importance'] = $aResult['importance'] + ($iCountWords*0.1); // 0.1 is a completely arbitrary number but something in the range 0.1 to 0.5 would seem right
1752
1753                                 $aResult['name'] = $aResult['langaddress'];
1754                                 // secondary ordering (for results with same importance (the smaller the better):
1755                                 //   - approximate importance of address parts
1756                                 $aResult['foundorder'] = -$aResult['addressimportance']/10;
1757                                 //   - number of exact matches from the query
1758                                 if (isset($this->exactMatchCache[$aResult['place_id']]))
1759                                         $aResult['foundorder'] -= $this->exactMatchCache[$aResult['place_id']];
1760                                 else if (isset($this->exactMatchCache[$aResult['parent_place_id']]))
1761                                         $aResult['foundorder'] -= $this->exactMatchCache[$aResult['parent_place_id']];
1762                                 //  - importance of the class/type
1763                                 if (isset($aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1764                                         && $aClassType[$aResult['class'].':'.$aResult['type']]['importance'])
1765                                 {
1766                                         $aResult['foundorder'] = $aResult['foundorder'] + 0.000001 * $aClassType[$aResult['class'].':'.$aResult['type']]['importance'];
1767                                 }
1768                                 else
1769                                 {
1770                                         $aResult['foundorder'] = $aResult['foundorder'] + 0.001;
1771                                 }
1772                                 $aSearchResults[$iResNum] = $aResult;
1773                         }
1774                         uasort($aSearchResults, 'byImportance');
1775
1776                         $aOSMIDDone = array();
1777                         $aClassTypeNameDone = array();
1778                         $aToFilter = $aSearchResults;
1779                         $aSearchResults = array();
1780
1781                         $bFirst = true;
1782                         foreach($aToFilter as $iResNum => $aResult)
1783                         {
1784                                 $this->aExcludePlaceIDs[$aResult['place_id']] = $aResult['place_id'];
1785                                 if ($bFirst)
1786                                 {
1787                                         $fLat = $aResult['lat'];
1788                                         $fLon = $aResult['lon'];
1789                                         if (isset($aResult['zoom'])) $iZoom = $aResult['zoom'];
1790                                         $bFirst = false;
1791                                 }
1792                                 if (!$this->bDeDupe || (!isset($aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']])
1793                                                         && !isset($aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']])))
1794                                 {
1795                                         $aOSMIDDone[$aResult['osm_type'].$aResult['osm_id']] = true;
1796                                         $aClassTypeNameDone[$aResult['osm_type'].$aResult['class'].$aResult['type'].$aResult['name'].$aResult['admin_level']] = true;
1797                                         $aSearchResults[] = $aResult;
1798                                 }
1799
1800                                 // Absolute limit on number of results
1801                                 if (sizeof($aSearchResults) >= $this->iFinalLimit) break;
1802                         }
1803
1804                         return $aSearchResults;
1805
1806                 } // end lookup()
1807
1808
1809         } // end class
1810