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