]> git.openstreetmap.org Git - nominatim.git/blob - lib/setup/SetupClass.php
Merge remote-tracking branch 'upstream/master'
[nominatim.git] / lib / setup / SetupClass.php
1 <?php
2
3 namespace Nominatim\Setup;
4
5 require_once(CONST_BasePath.'/lib/setup/AddressLevelParser.php');
6
7 class SetupFunctions
8 {
9     protected $iCacheMemory;
10     protected $iInstances;
11     protected $sModulePath;
12     protected $aDSNInfo;
13     protected $bVerbose;
14     protected $sIgnoreErrors;
15     protected $bEnableDiffUpdates;
16     protected $bEnableDebugStatements;
17     protected $bNoPartitions;
18     protected $oDB = null;
19
20     public function __construct(array $aCMDResult)
21     {
22         // by default, use all but one processor, but never more than 15.
23         $this->iInstances = isset($aCMDResult['threads'])
24             ? $aCMDResult['threads']
25             : (min(16, getProcessorCount()) - 1);
26
27         if ($this->iInstances < 1) {
28             $this->iInstances = 1;
29             warn('resetting threads to '.$this->iInstances);
30         }
31
32         // Assume we can steal all the cache memory in the box (unless told otherwise)
33         if (isset($aCMDResult['osm2pgsql-cache'])) {
34             $this->iCacheMemory = $aCMDResult['osm2pgsql-cache'];
35         } else {
36             $this->iCacheMemory = getCacheMemoryMB();
37         }
38
39         $this->sModulePath = CONST_Database_Module_Path;
40         info('module path: ' . $this->sModulePath);
41
42         // parse database string
43         $this->aDSNInfo = \Nominatim\DB::parseDSN(CONST_Database_DSN);
44         if (!isset($this->aDSNInfo['port'])) {
45             $this->aDSNInfo['port'] = 5432;
46         }
47
48         // setting member variables based on command line options stored in $aCMDResult
49         $this->bVerbose = $aCMDResult['verbose'];
50
51         //setting default values which are not set by the update.php array
52         if (isset($aCMDResult['ignore-errors'])) {
53             $this->sIgnoreErrors = $aCMDResult['ignore-errors'];
54         } else {
55             $this->sIgnoreErrors = false;
56         }
57         if (isset($aCMDResult['enable-debug-statements'])) {
58             $this->bEnableDebugStatements = $aCMDResult['enable-debug-statements'];
59         } else {
60             $this->bEnableDebugStatements = false;
61         }
62         if (isset($aCMDResult['no-partitions'])) {
63             $this->bNoPartitions = $aCMDResult['no-partitions'];
64         } else {
65             $this->bNoPartitions = false;
66         }
67         if (isset($aCMDResult['enable-diff-updates'])) {
68             $this->bEnableDiffUpdates = $aCMDResult['enable-diff-updates'];
69         } else {
70             $this->bEnableDiffUpdates = false;
71         }
72     }
73
74     public function createDB()
75     {
76         info('Create DB');
77         $oDB = new \Nominatim\DB;
78
79         if ($oDB->databaseExists()) {
80             fail('database already exists ('.CONST_Database_DSN.')');
81         }
82
83         $sCreateDBCmd = 'createdb -E UTF-8 -p '.$this->aDSNInfo['port'].' '.$this->aDSNInfo['database'];
84         if (isset($this->aDSNInfo['username'])) {
85             $sCreateDBCmd .= ' -U '.$this->aDSNInfo['username'];
86         }
87
88         if (isset($this->aDSNInfo['hostspec'])) {
89             $sCreateDBCmd .= ' -h '.$this->aDSNInfo['hostspec'];
90         }
91
92         $result = $this->runWithPgEnv($sCreateDBCmd);
93         if ($result != 0) fail('Error executing external command: '.$sCreateDBCmd);
94     }
95
96     public function connect()
97     {
98         $this->oDB = new \Nominatim\DB();
99         $this->oDB->connect();
100     }
101
102     public function setupDB()
103     {
104         info('Setup DB');
105
106         $fPostgresVersion = $this->oDB->getPostgresVersion();
107         echo 'Postgres version found: '.$fPostgresVersion."\n";
108
109         if ($fPostgresVersion < 9.01) {
110             fail('Minimum supported version of Postgresql is 9.1.');
111         }
112
113         $this->pgsqlRunScript('CREATE EXTENSION IF NOT EXISTS hstore');
114         $this->pgsqlRunScript('CREATE EXTENSION IF NOT EXISTS postgis');
115
116         // For extratags and namedetails the hstore_to_json converter is
117         // needed which is only available from Postgresql 9.3+. For older
118         // versions add a dummy function that returns nothing.
119         $iNumFunc = $this->oDB->getOne("select count(*) from pg_proc where proname = 'hstore_to_json'");
120
121         if ($iNumFunc == 0) {
122             $this->pgsqlRunScript("create function hstore_to_json(dummy hstore) returns text AS 'select null::text' language sql immutable");
123             warn('Postgresql is too old. extratags and namedetails API not available.');
124         }
125
126
127         $fPostgisVersion = $this->oDB->getPostgisVersion();
128         echo 'Postgis version found: '.$fPostgisVersion."\n";
129
130         if ($fPostgisVersion < 2.1) {
131             // Functions were renamed in 2.1 and throw an annoying deprecation warning
132             $this->pgsqlRunScript('ALTER FUNCTION st_line_interpolate_point(geometry, double precision) RENAME TO ST_LineInterpolatePoint');
133             $this->pgsqlRunScript('ALTER FUNCTION ST_Line_Locate_Point(geometry, geometry) RENAME TO ST_LineLocatePoint');
134         }
135         if ($fPostgisVersion < 2.2) {
136             $this->pgsqlRunScript('ALTER FUNCTION ST_Distance_Spheroid(geometry, geometry, spheroid) RENAME TO ST_DistanceSpheroid');
137         }
138
139         $i = $this->oDB->getOne("select count(*) from pg_user where usename = '".CONST_Database_Web_User."'");
140         if ($i == 0) {
141             echo "\nERROR: Web user '".CONST_Database_Web_User."' does not exist. Create it with:\n";
142             echo "\n          createuser ".CONST_Database_Web_User."\n\n";
143             exit(1);
144         }
145
146         // Try accessing the C module, so we know early if something is wrong
147         checkModulePresence(); // raises exception on failure
148
149         if (!file_exists(CONST_ExtraDataPath.'/country_osm_grid.sql.gz')) {
150             echo 'Error: you need to download the country_osm_grid first:';
151             echo "\n    wget -O ".CONST_ExtraDataPath."/country_osm_grid.sql.gz https://www.nominatim.org/data/country_grid.sql.gz\n";
152             exit(1);
153         }
154         $this->pgsqlRunScriptFile(CONST_BasePath.'/data/country_name.sql');
155         $this->pgsqlRunScriptFile(CONST_BasePath.'/data/country_osm_grid.sql.gz');
156         $this->pgsqlRunScriptFile(CONST_BasePath.'/data/gb_postcode_table.sql');
157
158         $sPostcodeFilename = CONST_BasePath.'/data/gb_postcode_data.sql.gz';
159         if (file_exists($sPostcodeFilename)) {
160             $this->pgsqlRunScriptFile($sPostcodeFilename);
161         } else {
162             warn('optional external UK postcode table file ('.$sPostcodeFilename.') not found. Skipping.');
163         }
164
165         if (CONST_Use_Extra_US_Postcodes) {
166             $this->pgsqlRunScriptFile(CONST_BasePath.'/data/us_postcode.sql');
167         }
168
169         if ($this->bNoPartitions) {
170             $this->pgsqlRunScript('update country_name set partition = 0');
171         }
172
173         // the following will be needed by createFunctions later but
174         // is only defined in the subsequently called createTables
175         // Create dummies here that will be overwritten by the proper
176         // versions in create-tables.
177         $this->pgsqlRunScript('CREATE TABLE IF NOT EXISTS place_boundingbox ()');
178         $this->pgsqlRunScript('CREATE TYPE wikipedia_article_match AS ()', false);
179     }
180
181     public function importData($sOSMFile)
182     {
183         info('Import data');
184
185         $osm2pgsql = CONST_Osm2pgsql_Binary;
186         if (!file_exists($osm2pgsql)) {
187             echo "Check CONST_Osm2pgsql_Binary in your local settings file.\n";
188             echo "Normally you should not need to set this manually.\n";
189             fail("osm2pgsql not found in '$osm2pgsql'");
190         }
191
192         $osm2pgsql .= ' -S '.CONST_Import_Style;
193
194         if (!is_null(CONST_Osm2pgsql_Flatnode_File) && CONST_Osm2pgsql_Flatnode_File) {
195             $osm2pgsql .= ' --flat-nodes '.CONST_Osm2pgsql_Flatnode_File;
196         }
197
198         if (CONST_Tablespace_Osm2pgsql_Data)
199             $osm2pgsql .= ' --tablespace-slim-data '.CONST_Tablespace_Osm2pgsql_Data;
200         if (CONST_Tablespace_Osm2pgsql_Index)
201             $osm2pgsql .= ' --tablespace-slim-index '.CONST_Tablespace_Osm2pgsql_Index;
202         if (CONST_Tablespace_Place_Data)
203             $osm2pgsql .= ' --tablespace-main-data '.CONST_Tablespace_Place_Data;
204         if (CONST_Tablespace_Place_Index)
205             $osm2pgsql .= ' --tablespace-main-index '.CONST_Tablespace_Place_Index;
206         $osm2pgsql .= ' -lsc -O gazetteer --hstore --number-processes 1';
207         $osm2pgsql .= ' -C '.$this->iCacheMemory;
208         $osm2pgsql .= ' -P '.$this->aDSNInfo['port'];
209         if (isset($this->aDSNInfo['username'])) {
210             $osm2pgsql .= ' -U '.$this->aDSNInfo['username'];
211         }
212         if (isset($this->aDSNInfo['hostspec'])) {
213             $osm2pgsql .= ' -H '.$this->aDSNInfo['hostspec'];
214         }
215         $osm2pgsql .= ' -d '.$this->aDSNInfo['database'].' '.$sOSMFile;
216
217         $this->runWithPgEnv($osm2pgsql);
218
219         if (!$this->sIgnoreErrors && !$this->oDB->getRow('select * from place limit 1')) {
220             fail('No Data');
221         }
222     }
223
224     public function createFunctions()
225     {
226         info('Create Functions');
227
228         // Try accessing the C module, so we know early if something is wrong
229         checkModulePresence(); // raises exception on failure
230
231         $this->createSqlFunctions();
232     }
233
234     public function createTables($bReverseOnly = false)
235     {
236         info('Create Tables');
237
238         $sTemplate = file_get_contents(CONST_BasePath.'/sql/tables.sql');
239         $sTemplate = str_replace('{www-user}', CONST_Database_Web_User, $sTemplate);
240         $sTemplate = $this->replaceTablespace(
241             '{ts:address-data}',
242             CONST_Tablespace_Address_Data,
243             $sTemplate
244         );
245         $sTemplate = $this->replaceTablespace(
246             '{ts:address-index}',
247             CONST_Tablespace_Address_Index,
248             $sTemplate
249         );
250         $sTemplate = $this->replaceTablespace(
251             '{ts:search-data}',
252             CONST_Tablespace_Search_Data,
253             $sTemplate
254         );
255         $sTemplate = $this->replaceTablespace(
256             '{ts:search-index}',
257             CONST_Tablespace_Search_Index,
258             $sTemplate
259         );
260         $sTemplate = $this->replaceTablespace(
261             '{ts:aux-data}',
262             CONST_Tablespace_Aux_Data,
263             $sTemplate
264         );
265         $sTemplate = $this->replaceTablespace(
266             '{ts:aux-index}',
267             CONST_Tablespace_Aux_Index,
268             $sTemplate
269         );
270
271         $this->pgsqlRunScript($sTemplate, false);
272
273         if ($bReverseOnly) {
274             $this->pgExec('DROP TABLE search_name');
275         }
276
277         $oAlParser = new AddressLevelParser(CONST_Address_Level_Config);
278         $oAlParser->createTable($this->oDB, 'address_levels');
279     }
280
281     public function createPartitionTables()
282     {
283         info('Create Partition Tables');
284
285         $sTemplate = file_get_contents(CONST_BasePath.'/sql/partition-tables.src.sql');
286         $sTemplate = $this->replaceTablespace(
287             '{ts:address-data}',
288             CONST_Tablespace_Address_Data,
289             $sTemplate
290         );
291
292         $sTemplate = $this->replaceTablespace(
293             '{ts:address-index}',
294             CONST_Tablespace_Address_Index,
295             $sTemplate
296         );
297
298         $sTemplate = $this->replaceTablespace(
299             '{ts:search-data}',
300             CONST_Tablespace_Search_Data,
301             $sTemplate
302         );
303
304         $sTemplate = $this->replaceTablespace(
305             '{ts:search-index}',
306             CONST_Tablespace_Search_Index,
307             $sTemplate
308         );
309
310         $sTemplate = $this->replaceTablespace(
311             '{ts:aux-data}',
312             CONST_Tablespace_Aux_Data,
313             $sTemplate
314         );
315
316         $sTemplate = $this->replaceTablespace(
317             '{ts:aux-index}',
318             CONST_Tablespace_Aux_Index,
319             $sTemplate
320         );
321
322         $this->pgsqlRunPartitionScript($sTemplate);
323     }
324
325     public function createPartitionFunctions()
326     {
327         info('Create Partition Functions');
328
329         $sTemplate = file_get_contents(CONST_BasePath.'/sql/partition-functions.src.sql');
330         $this->pgsqlRunPartitionScript($sTemplate);
331     }
332
333     public function importWikipediaArticles()
334     {
335         $sWikiArticlesFile = CONST_Wikipedia_Data_Path.'/wikipedia_article.sql.bin';
336         $sWikiRedirectsFile = CONST_Wikipedia_Data_Path.'/wikipedia_redirect.sql.bin';
337         if (file_exists($sWikiArticlesFile)) {
338             info('Importing wikipedia articles');
339             $this->pgsqlRunDropAndRestore($sWikiArticlesFile);
340         } else {
341             warn('wikipedia article dump file not found - places will have default importance');
342         }
343         if (file_exists($sWikiRedirectsFile)) {
344             info('Importing wikipedia redirects');
345             $this->pgsqlRunDropAndRestore($sWikiRedirectsFile);
346         } else {
347             warn('wikipedia redirect dump file not found - some place importance values may be missing');
348         }
349     }
350
351     public function loadData($bDisableTokenPrecalc)
352     {
353         info('Drop old Data');
354
355         $this->pgExec('TRUNCATE word');
356         echo '.';
357         $this->pgExec('TRUNCATE placex');
358         echo '.';
359         $this->pgExec('TRUNCATE location_property_osmline');
360         echo '.';
361         $this->pgExec('TRUNCATE place_addressline');
362         echo '.';
363         $this->pgExec('TRUNCATE place_boundingbox');
364         echo '.';
365         $this->pgExec('TRUNCATE location_area');
366         echo '.';
367         if (!$this->dbReverseOnly()) {
368             $this->pgExec('TRUNCATE search_name');
369             echo '.';
370         }
371         $this->pgExec('TRUNCATE search_name_blank');
372         echo '.';
373         $this->pgExec('DROP SEQUENCE seq_place');
374         echo '.';
375         $this->pgExec('CREATE SEQUENCE seq_place start 100000');
376         echo '.';
377
378         $sSQL = 'select distinct partition from country_name';
379         $aPartitions = $this->oDB->getCol($sSQL);
380
381         if (!$this->bNoPartitions) $aPartitions[] = 0;
382         foreach ($aPartitions as $sPartition) {
383             $this->pgExec('TRUNCATE location_road_'.$sPartition);
384             echo '.';
385         }
386
387         // used by getorcreate_word_id to ignore frequent partial words
388         $sSQL = 'CREATE OR REPLACE FUNCTION get_maxwordfreq() RETURNS integer AS ';
389         $sSQL .= '$$ SELECT '.CONST_Max_Word_Frequency.' as maxwordfreq; $$ LANGUAGE SQL IMMUTABLE';
390         $this->pgExec($sSQL);
391         echo ".\n";
392
393         // pre-create the word list
394         if (!$bDisableTokenPrecalc) {
395             info('Loading word list');
396             $this->pgsqlRunScriptFile(CONST_BasePath.'/data/words.sql');
397         }
398
399         info('Load Data');
400         $sColumns = 'osm_type, osm_id, class, type, name, admin_level, address, extratags, geometry';
401
402         $aDBInstances = array();
403         $iLoadThreads = max(1, $this->iInstances - 1);
404         for ($i = 0; $i < $iLoadThreads; $i++) {
405             // https://secure.php.net/manual/en/function.pg-connect.php
406             $DSN = CONST_Database_DSN;
407             $DSN = preg_replace('/^pgsql:/', '', $DSN);
408             $DSN = preg_replace('/;/', ' ', $DSN);
409             $aDBInstances[$i] = pg_connect($DSN, PGSQL_CONNECT_FORCE_NEW);
410             pg_ping($aDBInstances[$i]);
411         }
412
413         for ($i = 0; $i < $iLoadThreads; $i++) {
414             $sSQL = "INSERT INTO placex ($sColumns) SELECT $sColumns FROM place WHERE osm_id % $iLoadThreads = $i";
415             $sSQL .= " and not (class='place' and type='houses' and osm_type='W'";
416             $sSQL .= "          and ST_GeometryType(geometry) = 'ST_LineString')";
417             $sSQL .= ' and ST_IsValid(geometry)';
418             if ($this->bVerbose) echo "$sSQL\n";
419             if (!pg_send_query($aDBInstances[$i], $sSQL)) {
420                 fail(pg_last_error($aDBInstances[$i]));
421             }
422         }
423
424         // last thread for interpolation lines
425         // https://secure.php.net/manual/en/function.pg-connect.php
426         $DSN = CONST_Database_DSN;
427         $DSN = preg_replace('/^pgsql:/', '', $DSN);
428         $DSN = preg_replace('/;/', ' ', $DSN);
429         $aDBInstances[$iLoadThreads] = pg_connect($DSN, PGSQL_CONNECT_FORCE_NEW);
430         pg_ping($aDBInstances[$iLoadThreads]);
431         $sSQL = 'insert into location_property_osmline';
432         $sSQL .= ' (osm_id, address, linegeo)';
433         $sSQL .= ' SELECT osm_id, address, geometry from place where ';
434         $sSQL .= "class='place' and type='houses' and osm_type='W' and ST_GeometryType(geometry) = 'ST_LineString'";
435         if ($this->bVerbose) echo "$sSQL\n";
436         if (!pg_send_query($aDBInstances[$iLoadThreads], $sSQL)) {
437             fail(pg_last_error($aDBInstances[$iLoadThreads]));
438         }
439
440         $bFailed = false;
441         for ($i = 0; $i <= $iLoadThreads; $i++) {
442             while (($hPGresult = pg_get_result($aDBInstances[$i])) !== false) {
443                 $resultStatus = pg_result_status($hPGresult);
444                 // PGSQL_EMPTY_QUERY, PGSQL_COMMAND_OK, PGSQL_TUPLES_OK,
445                 // PGSQL_COPY_OUT, PGSQL_COPY_IN, PGSQL_BAD_RESPONSE,
446                 // PGSQL_NONFATAL_ERROR and PGSQL_FATAL_ERROR
447                 // echo 'Query result ' . $i . ' is: ' . $resultStatus . "\n";
448                 if ($resultStatus != PGSQL_COMMAND_OK && $resultStatus != PGSQL_TUPLES_OK) {
449                     $resultError = pg_result_error($hPGresult);
450                     echo '-- error text ' . $i . ': ' . $resultError . "\n";
451                     $bFailed = true;
452                 }
453             }
454         }
455         if ($bFailed) {
456             fail('SQL errors loading placex and/or location_property_osmline tables');
457         }
458
459         for ($i = 0; $i < $this->iInstances; $i++) {
460             pg_close($aDBInstances[$i]);
461         }
462
463         echo "\n";
464         info('Reanalysing database');
465         $this->pgsqlRunScript('ANALYSE');
466
467         $sDatabaseDate = getDatabaseDate($this->oDB);
468         $this->oDB->exec('TRUNCATE import_status');
469         if (!$sDatabaseDate) {
470             warn('could not determine database date.');
471         } else {
472             $sSQL = "INSERT INTO import_status (lastimportdate) VALUES('".$sDatabaseDate."')";
473             $this->oDB->exec($sSQL);
474             echo "Latest data imported from $sDatabaseDate.\n";
475         }
476     }
477
478     public function importTigerData()
479     {
480         info('Import Tiger data');
481
482         $sTemplate = file_get_contents(CONST_BasePath.'/sql/tiger_import_start.sql');
483         $sTemplate = str_replace('{www-user}', CONST_Database_Web_User, $sTemplate);
484         $sTemplate = $this->replaceTablespace(
485             '{ts:aux-data}',
486             CONST_Tablespace_Aux_Data,
487             $sTemplate
488         );
489         $sTemplate = $this->replaceTablespace(
490             '{ts:aux-index}',
491             CONST_Tablespace_Aux_Index,
492             $sTemplate
493         );
494         $this->pgsqlRunScript($sTemplate, false);
495
496         $aDBInstances = array();
497         for ($i = 0; $i < $this->iInstances; $i++) {
498             // https://secure.php.net/manual/en/function.pg-connect.php
499             $DSN = CONST_Database_DSN;
500             $DSN = preg_replace('/^pgsql:/', '', $DSN);
501             $DSN = preg_replace('/;/', ' ', $DSN);
502             $aDBInstances[$i] = pg_connect($DSN, PGSQL_CONNECT_FORCE_NEW | PGSQL_CONNECT_ASYNC);
503             pg_ping($aDBInstances[$i]);
504         }
505
506         foreach (glob(CONST_Tiger_Data_Path.'/*.sql') as $sFile) {
507             echo $sFile.': ';
508             $hFile = fopen($sFile, 'r');
509             $sSQL = fgets($hFile, 100000);
510             $iLines = 0;
511             while (true) {
512                 for ($i = 0; $i < $this->iInstances; $i++) {
513                     if (!pg_connection_busy($aDBInstances[$i])) {
514                         while (pg_get_result($aDBInstances[$i]));
515                         $sSQL = fgets($hFile, 100000);
516                         if (!$sSQL) break 2;
517                         if (!pg_send_query($aDBInstances[$i], $sSQL)) fail(pg_last_error($aDBInstances[$i]));
518                         $iLines++;
519                         if ($iLines == 1000) {
520                             echo '.';
521                             $iLines = 0;
522                         }
523                     }
524                 }
525                 usleep(10);
526             }
527             fclose($hFile);
528
529             $bAnyBusy = true;
530             while ($bAnyBusy) {
531                 $bAnyBusy = false;
532                 for ($i = 0; $i < $this->iInstances; $i++) {
533                     if (pg_connection_busy($aDBInstances[$i])) $bAnyBusy = true;
534                 }
535                 usleep(10);
536             }
537             echo "\n";
538         }
539
540         for ($i = 0; $i < $this->iInstances; $i++) {
541             pg_close($aDBInstances[$i]);
542         }
543
544         info('Creating indexes on Tiger data');
545         $sTemplate = file_get_contents(CONST_BasePath.'/sql/tiger_import_finish.sql');
546         $sTemplate = str_replace('{www-user}', CONST_Database_Web_User, $sTemplate);
547         $sTemplate = $this->replaceTablespace(
548             '{ts:aux-data}',
549             CONST_Tablespace_Aux_Data,
550             $sTemplate
551         );
552         $sTemplate = $this->replaceTablespace(
553             '{ts:aux-index}',
554             CONST_Tablespace_Aux_Index,
555             $sTemplate
556         );
557         $this->pgsqlRunScript($sTemplate, false);
558     }
559
560     public function calculatePostcodes($bCMDResultAll)
561     {
562         info('Calculate Postcodes');
563         $this->pgExec('TRUNCATE location_postcode');
564
565         $sSQL  = 'INSERT INTO location_postcode';
566         $sSQL .= ' (place_id, indexed_status, country_code, postcode, geometry) ';
567         $sSQL .= "SELECT nextval('seq_place'), 1, country_code,";
568         $sSQL .= "       upper(trim (both ' ' from address->'postcode')) as pc,";
569         $sSQL .= '       ST_Centroid(ST_Collect(ST_Centroid(geometry)))';
570         $sSQL .= '  FROM placex';
571         $sSQL .= " WHERE address ? 'postcode' AND address->'postcode' NOT SIMILAR TO '%(,|;)%'";
572         $sSQL .= '       AND geometry IS NOT null';
573         $sSQL .= ' GROUP BY country_code, pc';
574         $this->pgExec($sSQL);
575
576         if (CONST_Use_Extra_US_Postcodes) {
577             // only add postcodes that are not yet available in OSM
578             $sSQL  = 'INSERT INTO location_postcode';
579             $sSQL .= ' (place_id, indexed_status, country_code, postcode, geometry) ';
580             $sSQL .= "SELECT nextval('seq_place'), 1, 'us', postcode,";
581             $sSQL .= '       ST_SetSRID(ST_Point(x,y),4326)';
582             $sSQL .= '  FROM us_postcode WHERE postcode NOT IN';
583             $sSQL .= '        (SELECT postcode FROM location_postcode';
584             $sSQL .= "          WHERE country_code = 'us')";
585             $this->pgExec($sSQL);
586         }
587
588         // add missing postcodes for GB (if available)
589         $sSQL  = 'INSERT INTO location_postcode';
590         $sSQL .= ' (place_id, indexed_status, country_code, postcode, geometry) ';
591         $sSQL .= "SELECT nextval('seq_place'), 1, 'gb', postcode, geometry";
592         $sSQL .= '  FROM gb_postcode WHERE postcode NOT IN';
593         $sSQL .= '           (SELECT postcode FROM location_postcode';
594         $sSQL .= "             WHERE country_code = 'gb')";
595         $this->pgExec($sSQL);
596
597         if (!$bCMDResultAll) {
598             $sSQL = "DELETE FROM word WHERE class='place' and type='postcode'";
599             $sSQL .= 'and word NOT IN (SELECT postcode FROM location_postcode)';
600             $this->pgExec($sSQL);
601         }
602
603         $sSQL = 'SELECT count(getorcreate_postcode_id(v)) FROM ';
604         $sSQL .= '(SELECT distinct(postcode) as v FROM location_postcode) p';
605         $this->pgExec($sSQL);
606     }
607
608     public function index($bIndexNoanalyse)
609     {
610         $sOutputFile = '';
611         $sBaseCmd = CONST_InstallPath.'/nominatim/nominatim -i -d '.$this->aDSNInfo['database'].' -P '
612             .$this->aDSNInfo['port'].' -t '.$this->iInstances.$sOutputFile;
613         if (isset($this->aDSNInfo['hostspec'])) {
614             $sBaseCmd .= ' -H '.$this->aDSNInfo['hostspec'];
615         }
616         if (isset($this->aDSNInfo['username'])) {
617             $sBaseCmd .= ' -U '.$this->aDSNInfo['username'];
618         }
619
620         info('Index ranks 0 - 4');
621         $iStatus = $this->runWithPgEnv($sBaseCmd.' -R 4');
622         if ($iStatus != 0) {
623             fail('error status ' . $iStatus . ' running nominatim!');
624         }
625         if (!$bIndexNoanalyse) $this->pgsqlRunScript('ANALYSE');
626
627         info('Index ranks 5 - 25');
628         $iStatus = $this->runWithPgEnv($sBaseCmd.' -r 5 -R 25');
629         if ($iStatus != 0) {
630             fail('error status ' . $iStatus . ' running nominatim!');
631         }
632         if (!$bIndexNoanalyse) $this->pgsqlRunScript('ANALYSE');
633
634         info('Index ranks 26 - 30');
635         $iStatus = $this->runWithPgEnv($sBaseCmd.' -r 26');
636         if ($iStatus != 0) {
637             fail('error status ' . $iStatus . ' running nominatim!');
638         }
639
640         info('Index postcodes');
641         $sSQL = 'UPDATE location_postcode SET indexed_status = 0';
642         $this->pgExec($sSQL);
643     }
644
645     public function createSearchIndices()
646     {
647         info('Create Search indices');
648
649         $sTemplate = file_get_contents(CONST_BasePath.'/sql/indices.src.sql');
650         if (!$this->dbReverseOnly()) {
651             $sTemplate .= file_get_contents(CONST_BasePath.'/sql/indices_search.src.sql');
652         }
653         $sTemplate = str_replace('{www-user}', CONST_Database_Web_User, $sTemplate);
654         $sTemplate = $this->replaceTablespace(
655             '{ts:address-index}',
656             CONST_Tablespace_Address_Index,
657             $sTemplate
658         );
659         $sTemplate = $this->replaceTablespace(
660             '{ts:search-index}',
661             CONST_Tablespace_Search_Index,
662             $sTemplate
663         );
664         $sTemplate = $this->replaceTablespace(
665             '{ts:aux-index}',
666             CONST_Tablespace_Aux_Index,
667             $sTemplate
668         );
669         $this->pgsqlRunScript($sTemplate);
670     }
671
672     public function createCountryNames()
673     {
674         info('Create search index for default country names');
675
676         $this->pgsqlRunScript("select getorcreate_country(make_standard_name('uk'), 'gb')");
677         $this->pgsqlRunScript("select getorcreate_country(make_standard_name('united states'), 'us')");
678         $this->pgsqlRunScript('select count(*) from (select getorcreate_country(make_standard_name(country_code), country_code) from country_name where country_code is not null) as x');
679         $this->pgsqlRunScript("select count(*) from (select getorcreate_country(make_standard_name(name->'name'), country_code) from country_name where name ? 'name') as x");
680         $sSQL = 'select count(*) from (select getorcreate_country(make_standard_name(v),'
681             .'country_code) from (select country_code, skeys(name) as k, svals(name) as v from country_name) x where k ';
682         if (CONST_Languages) {
683             $sSQL .= 'in ';
684             $sDelim = '(';
685             foreach (explode(',', CONST_Languages) as $sLang) {
686                 $sSQL .= $sDelim."'name:$sLang'";
687                 $sDelim = ',';
688             }
689             $sSQL .= ')';
690         } else {
691             // all include all simple name tags
692             $sSQL .= "like 'name:%'";
693         }
694         $sSQL .= ') v';
695         $this->pgsqlRunScript($sSQL);
696     }
697
698     public function drop()
699     {
700         info('Drop tables only required for updates');
701
702         // The implementation is potentially a bit dangerous because it uses
703         // a positive selection of tables to keep, and deletes everything else.
704         // Including any tables that the unsuspecting user might have manually
705         // created. USE AT YOUR OWN PERIL.
706         // tables we want to keep. everything else goes.
707         $aKeepTables = array(
708                         '*columns',
709                         'import_polygon_*',
710                         'import_status',
711                         'place_addressline',
712                         'location_postcode',
713                         'location_property*',
714                         'placex',
715                         'search_name',
716                         'seq_*',
717                         'word',
718                         'query_log',
719                         'new_query_log',
720                         'spatial_ref_sys',
721                         'country_name',
722                         'place_classtype_*',
723                         'country_osm_grid'
724                        );
725
726         $aDropTables = array();
727         $aHaveTables = $this->oDB->getCol("SELECT tablename FROM pg_tables WHERE schemaname='public'");
728
729         foreach ($aHaveTables as $sTable) {
730             $bFound = false;
731             foreach ($aKeepTables as $sKeep) {
732                 if (fnmatch($sKeep, $sTable)) {
733                     $bFound = true;
734                     break;
735                 }
736             }
737             if (!$bFound) array_push($aDropTables, $sTable);
738         }
739         foreach ($aDropTables as $sDrop) {
740             if ($this->bVerbose) echo "Dropping table $sDrop\n";
741             $this->oDB->exec("DROP TABLE $sDrop CASCADE");
742             // ignore warnings/errors as they might be caused by a table having
743             // been deleted already by CASCADE
744         }
745
746         if (!is_null(CONST_Osm2pgsql_Flatnode_File) && CONST_Osm2pgsql_Flatnode_File) {
747             if (file_exists(CONST_Osm2pgsql_Flatnode_File)) {
748                 if ($this->bVerbose) echo 'Deleting '.CONST_Osm2pgsql_Flatnode_File."\n";
749                 unlink(CONST_Osm2pgsql_Flatnode_File);
750             }
751         }
752     }
753
754     private function pgsqlRunDropAndRestore($sDumpFile)
755     {
756         $sCMD = 'pg_restore -p '.$this->aDSNInfo['port'].' -d '.$this->aDSNInfo['database'].' -Fc --clean '.$sDumpFile;
757         if (isset($this->aDSNInfo['hostspec'])) {
758             $sCMD .= ' -h '.$this->aDSNInfo['hostspec'];
759         }
760         if (isset($this->aDSNInfo['username'])) {
761             $sCMD .= ' -U '.$this->aDSNInfo['username'];
762         }
763
764         $this->runWithPgEnv($sCMD);
765     }
766
767     private function pgsqlRunScript($sScript, $bfatal = true)
768     {
769         runSQLScript(
770             $sScript,
771             $bfatal,
772             $this->bVerbose,
773             $this->sIgnoreErrors
774         );
775     }
776
777     private function createSqlFunctions()
778     {
779         $sTemplate = file_get_contents(CONST_BasePath.'/sql/functions.sql');
780         $sTemplate = str_replace('{modulepath}', $this->sModulePath, $sTemplate);
781         if ($this->bEnableDiffUpdates) {
782             $sTemplate = str_replace('RETURN NEW; -- %DIFFUPDATES%', '--', $sTemplate);
783         }
784         if ($this->bEnableDebugStatements) {
785             $sTemplate = str_replace('--DEBUG:', '', $sTemplate);
786         }
787         if (CONST_Limit_Reindexing) {
788             $sTemplate = str_replace('--LIMIT INDEXING:', '', $sTemplate);
789         }
790         if (!CONST_Use_US_Tiger_Data) {
791             $sTemplate = str_replace('-- %NOTIGERDATA% ', '', $sTemplate);
792         }
793         if (!CONST_Use_Aux_Location_data) {
794             $sTemplate = str_replace('-- %NOAUXDATA% ', '', $sTemplate);
795         }
796
797         $sReverseOnly = $this->dbReverseOnly() ? 'true' : 'false';
798         $sTemplate = str_replace('%REVERSE-ONLY%', $sReverseOnly, $sTemplate);
799
800         $this->pgsqlRunScript($sTemplate);
801     }
802
803     private function pgsqlRunPartitionScript($sTemplate)
804     {
805         $sSQL = 'select distinct partition from country_name';
806         $aPartitions = $this->oDB->getCol($sSQL);
807         if (!$this->bNoPartitions) $aPartitions[] = 0;
808
809         preg_match_all('#^-- start(.*?)^-- end#ms', $sTemplate, $aMatches, PREG_SET_ORDER);
810         foreach ($aMatches as $aMatch) {
811             $sResult = '';
812             foreach ($aPartitions as $sPartitionName) {
813                 $sResult .= str_replace('-partition-', $sPartitionName, $aMatch[1]);
814             }
815             $sTemplate = str_replace($aMatch[0], $sResult, $sTemplate);
816         }
817
818         $this->pgsqlRunScript($sTemplate);
819     }
820
821     private function pgsqlRunScriptFile($sFilename)
822     {
823         if (!file_exists($sFilename)) fail('unable to find '.$sFilename);
824
825         $sCMD = 'psql -p '.$this->aDSNInfo['port'].' -d '.$this->aDSNInfo['database'];
826         if (!$this->bVerbose) {
827             $sCMD .= ' -q';
828         }
829         if (isset($this->aDSNInfo['hostspec'])) {
830             $sCMD .= ' -h '.$this->aDSNInfo['hostspec'];
831         }
832         if (isset($this->aDSNInfo['username'])) {
833             $sCMD .= ' -U '.$this->aDSNInfo['username'];
834         }
835         $aProcEnv = null;
836         if (isset($this->aDSNInfo['password'])) {
837             $aProcEnv = array_merge(array('PGPASSWORD' => $this->aDSNInfo['password']), $_ENV);
838         }
839         $ahGzipPipes = null;
840         if (preg_match('/\\.gz$/', $sFilename)) {
841             $aDescriptors = array(
842                              0 => array('pipe', 'r'),
843                              1 => array('pipe', 'w'),
844                              2 => array('file', '/dev/null', 'a')
845                             );
846             $hGzipProcess = proc_open('zcat '.$sFilename, $aDescriptors, $ahGzipPipes);
847             if (!is_resource($hGzipProcess)) fail('unable to start zcat');
848             $aReadPipe = $ahGzipPipes[1];
849             fclose($ahGzipPipes[0]);
850         } else {
851             $sCMD .= ' -f '.$sFilename;
852             $aReadPipe = array('pipe', 'r');
853         }
854         $aDescriptors = array(
855                          0 => $aReadPipe,
856                          1 => array('pipe', 'w'),
857                          2 => array('file', '/dev/null', 'a')
858                         );
859         $ahPipes = null;
860         $hProcess = proc_open($sCMD, $aDescriptors, $ahPipes, null, $aProcEnv);
861         if (!is_resource($hProcess)) fail('unable to start pgsql');
862         // TODO: error checking
863         while (!feof($ahPipes[1])) {
864             echo fread($ahPipes[1], 4096);
865         }
866         fclose($ahPipes[1]);
867         $iReturn = proc_close($hProcess);
868         if ($iReturn > 0) {
869             fail("pgsql returned with error code ($iReturn)");
870         }
871         if ($ahGzipPipes) {
872             fclose($ahGzipPipes[1]);
873             proc_close($hGzipProcess);
874         }
875     }
876
877     private function replaceTablespace($sTemplate, $sTablespace, $sSql)
878     {
879         if ($sTablespace) {
880             $sSql = str_replace($sTemplate, 'TABLESPACE "'.$sTablespace.'"', $sSql);
881         } else {
882             $sSql = str_replace($sTemplate, '', $sSql);
883         }
884         return $sSql;
885     }
886
887     private function runWithPgEnv($sCmd)
888     {
889         if ($this->bVerbose) {
890             echo "Execute: $sCmd\n";
891         }
892
893         $aProcEnv = null;
894
895         if (isset($this->aDSNInfo['password'])) {
896             $aProcEnv = array_merge(array('PGPASSWORD' => $this->aDSNInfo['password']), $_ENV);
897         }
898
899         return runWithEnv($sCmd, $aProcEnv);
900     }
901
902     /**
903      * Execute the SQL command on the open database.
904      *
905      * @param string $sSQL SQL command to execute.
906      *
907      * @return null
908      *
909      * @pre connect() must have been called.
910      */
911     private function pgExec($sSQL)
912     {
913         $this->oDB->exec($sSQL);
914     }
915
916     /**
917      * Check if the database is in reverse-only mode.
918      *
919      * @return True if there is no search_name table and infrastructure.
920      */
921     private function dbReverseOnly()
922     {
923         return !($this->oDB->tableExists('search_name'));
924     }
925 }