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