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