diff --git a/plugins/CustomVariables/Archiving.php b/plugins/CustomVariables/Archiving.php
index 166d8c912480a6c283296bcf3888d59f1d4d96a3..c85d230b3b41e84dc459b8b16f988e5d8c5065a5 100644
--- a/plugins/CustomVariables/Archiving.php
+++ b/plugins/CustomVariables/Archiving.php
@@ -27,10 +27,15 @@ class Piwik_CustomVariables_Archiving
 
     public function archiveDay(Piwik_ArchiveProcessing_Day $archiveProcessing)
     {
-        $this->archiveDayAggregate($archiveProcessing);
+        for ($i = 1; $i <= Piwik_Tracker::MAX_CUSTOM_VARIABLES; $i++) {
+            $this->aggregateCustomVariable($archiveProcessing, $i);
+        }
 
-        $table = $archiveProcessing->getDataTableWithSubtablesFromArraysIndexedByLabel($this->metricsByKeyAndValue, $this->metricsByKey);
+        $this->removeVisitsMetricsFromActionsAggregate($archiveProcessing);
+        $archiveProcessing->enrichConversionsByLabelArray($this->metricsByKey);
+        $archiveProcessing->enrichConversionsByLabelArrayHasTwoLevels($this->metricsByKeyAndValue);
 
+        $table = $archiveProcessing->getDataTableWithSubtablesFromArraysIndexedByLabel($this->metricsByKeyAndValue, $this->metricsByKey);
         $blob = $table->getSerialized(
             $this->maximumRowsInDataTableLevelZero, $this->maximumRowsInSubDataTable,
             $columnToSort = Piwik_Archive::INDEX_NB_VISITS
@@ -39,21 +44,6 @@ class Piwik_CustomVariables_Archiving
         $archiveProcessing->insertBlobRecord(self::BLOB_NAME, $blob);
     }
 
-    /**
-     * @param Piwik_ArchiveProcessing_Day $archiveProcessing
-     * @return void
-     */
-    protected function archiveDayAggregate(Piwik_ArchiveProcessing_Day $archiveProcessing)
-    {
-        for ($i = 1; $i <= Piwik_Tracker::MAX_CUSTOM_VARIABLES; $i++) {
-            $this->aggregateCustomVariable($archiveProcessing, $i);
-        }
-
-        $this->removeVisitsMetricsFromActionsAggregate($archiveProcessing);
-        $archiveProcessing->enrichConversionsByLabelArray($this->metricsByKey);
-        $archiveProcessing->enrichConversionsByLabelArrayHasTwoLevels($this->metricsByKeyAndValue);
-    }
-
     /**
      * @param Piwik_ArchiveProcessing_Day $archiveProcessing
      * @param $slot
diff --git a/plugins/Goals/Archiving.php b/plugins/Goals/Archiving.php
new file mode 100644
index 0000000000000000000000000000000000000000..c99ba4d48d7942f739042252245f1d81807f0de5
--- /dev/null
+++ b/plugins/Goals/Archiving.php
@@ -0,0 +1,346 @@
+<?php
+
+class Piwik_Goals_Archiving
+{
+
+    const VISITS_UNTIL_RECORD_NAME = 'visits_until_conv';
+    const DAYS_UNTIL_CONV_RECORD_NAME = 'days_until_conv';
+    /**
+     * This array stores the ranges to use when displaying the 'visits to conversion' report
+     */
+    public static $visitCountRanges = array(
+        array(1, 1),
+        array(2, 2),
+        array(3, 3),
+        array(4, 4),
+        array(5, 5),
+        array(6, 6),
+        array(7, 7),
+        array(8, 8),
+        array(9, 14),
+        array(15, 25),
+        array(26, 50),
+        array(51, 100),
+        array(100)
+    );
+    /**
+     * This array stores the ranges to use when displaying the 'days to conversion' report
+     */
+    public static $daysToConvRanges = array(
+        array(0, 0),
+        array(1, 1),
+        array(2, 2),
+        array(3, 3),
+        array(4, 4),
+        array(5, 5),
+        array(6, 6),
+        array(7, 7),
+        array(8, 14),
+        array(15, 30),
+        array(31, 60),
+        array(61, 120),
+        array(121, 364),
+        array(364)
+    );
+    protected $dimensions = array(
+        'idaction_sku'      => 'Goals_ItemsSku',
+        'idaction_name'     => 'Goals_ItemsName',
+        'idaction_category' => 'Goals_ItemsCategory'
+    );
+
+    /**
+     * @param $archiveProcessing
+     */
+    public function archiveDay($archiveProcessing)
+    {
+        $this->archiveGeneralGoalMetrics($archiveProcessing);
+        $this->archiveEcommerceItems($archiveProcessing);
+    }
+
+    /**
+     * @param Piwik_ArchiveProcessing_Day $archiveProcessing
+     */
+    function archiveGeneralGoalMetrics($archiveProcessing)
+    {
+        // extra aggregate selects for the visits to conversion report
+        $visitToConvExtraCols = Piwik_ArchiveProcessing_Day::buildReduceByRangeSelect(
+            'visitor_count_visits', self::$visitCountRanges, 'log_conversion', 'vcv');
+
+        // extra aggregate selects for the days to conversion report
+        $daysToConvExtraCols = Piwik_ArchiveProcessing_Day::buildReduceByRangeSelect(
+            'visitor_days_since_first', self::$daysToConvRanges, 'log_conversion', 'vdsf');
+
+        $query = $archiveProcessing->queryConversionsByDimension(
+            array(), '', array_merge($visitToConvExtraCols, $daysToConvExtraCols));
+
+        if ($query === false) {
+            return;
+        }
+
+        $goals = array();
+        $visitsToConvReport = array();
+        $daysToConvReport = array();
+
+        // Get a standard empty goal row
+        $overall = $archiveProcessing->makeEmptyGoalRow($idGoal = 1);
+        while ($row = $query->fetch()) {
+            $idgoal = $row['idgoal'];
+
+            if (!isset($goals[$idgoal])) {
+                $goals[$idgoal] = $archiveProcessing->makeEmptyGoalRow($idgoal);
+
+                $visitsToConvReport[$idgoal] = new Piwik_DataTable();
+                $daysToConvReport[$idgoal] = new Piwik_DataTable();
+            }
+            $archiveProcessing->sumGoalMetrics($row, $goals[$idgoal]);
+
+            // We don't want to sum Abandoned cart metrics in the overall revenue/conversions/converted visits
+            // since it is a "negative conversion"
+            if ($idgoal != Piwik_Tracker_GoalManager::IDGOAL_CART) {
+                $archiveProcessing->sumGoalMetrics($row, $overall);
+            }
+
+            // map the goal + visit number of a visitor with the # of conversions that happened on that visit
+            $table = $archiveProcessing->getSimpleDataTableFromRow($row, Piwik_Archive::INDEX_NB_CONVERSIONS, 'vcv');
+            $visitsToConvReport[$idgoal]->addDataTable($table);
+
+            // map the goal + day number of a visit with the # of conversion that happened on that day
+            $table = $archiveProcessing->getSimpleDataTableFromRow($row, Piwik_Archive::INDEX_NB_CONVERSIONS, 'vdsf');
+            $daysToConvReport[$idgoal]->addDataTable($table);
+        }
+
+        // these data tables hold reports for every goal of a site
+        $visitsToConvOverview = new Piwik_DataTable();
+        $daysToConvOverview = new Piwik_DataTable();
+
+        // Stats by goal, for all visitors
+        foreach ($goals as $idgoal => $values) {
+            foreach ($values as $metricId => $value) {
+                $metricName = Piwik_Archive::$mappingFromIdToNameGoal[$metricId];
+                $recordName = self::getRecordName($metricName, $idgoal);
+                $archiveProcessing->insertNumericRecord($recordName, $value);
+            }
+            $conversion_rate = $this->getConversionRate($values[Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED], $archiveProcessing);
+            $recordName = self::getRecordName('conversion_rate', $idgoal);
+            $archiveProcessing->insertNumericRecord($recordName, $conversion_rate);
+
+            // if the goal is not a special goal (like ecommerce) add it to the overview report
+            if ($idgoal !== Piwik_Tracker_GoalManager::IDGOAL_CART &&
+                $idgoal !== Piwik_Tracker_GoalManager::IDGOAL_ORDER
+            ) {
+                $visitsToConvOverview->addDataTable($visitsToConvReport[$idgoal]);
+                $daysToConvOverview->addDataTable($daysToConvReport[$idgoal]);
+            }
+
+            // visit count until conversion stats
+            $archiveProcessing->insertBlobRecord(
+                self::getRecordName(self::VISITS_UNTIL_RECORD_NAME, $idgoal),
+                $visitsToConvReport[$idgoal]->getSerialized());
+
+            // day count until conversion stats
+            $archiveProcessing->insertBlobRecord(
+                self::getRecordName(self::DAYS_UNTIL_CONV_RECORD_NAME, $idgoal),
+                $daysToConvReport[$idgoal]->getSerialized());
+        }
+
+        // archive overview reports
+        $archiveProcessing->insertBlobRecord(
+            self::getRecordName(self::VISITS_UNTIL_RECORD_NAME), $visitsToConvOverview->getSerialized());
+        $archiveProcessing->insertBlobRecord(
+            self::getRecordName(self::DAYS_UNTIL_CONV_RECORD_NAME), $daysToConvOverview->getSerialized());
+
+        // Stats for all goals
+        $totalAllGoals = array(
+            self::getRecordName('conversion_rate')     => $this->getConversionRate($archiveProcessing->getNumberOfVisitsConverted(), $archiveProcessing),
+            self::getRecordName('nb_conversions')      => $overall[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS],
+            self::getRecordName('nb_visits_converted') => $archiveProcessing->getNumberOfVisitsConverted(),
+            self::getRecordName('revenue')             => $overall[Piwik_Archive::INDEX_GOAL_REVENUE],
+        );
+        foreach ($totalAllGoals as $recordName => $value) {
+            $archiveProcessing->insertNumericRecord($recordName, $value);
+        }
+    }
+
+    /**
+     * @param string $recordName 'nb_conversions'
+     * @param int|bool $idGoal idGoal to return the metrics for, or false to return overall
+     * @return string Archive record name
+     */
+    static public function getRecordName($recordName, $idGoal = false)
+    {
+        $idGoalStr = '';
+        if ($idGoal !== false) {
+            $idGoalStr = $idGoal . "_";
+        }
+        return 'Goal_' . $idGoalStr . $recordName;
+    }
+
+    private function getConversionRate($count, $archiveProcessing)
+    {
+        $visits = $archiveProcessing->getNumberOfVisits();
+        return round(100 * $count / $visits, Piwik_Tracker_GoalManager::REVENUE_PRECISION);
+    }
+
+    /**
+     * @param Piwik_ArchiveProcessing_Day $archiveProcessing
+     */
+    function archiveEcommerceItems($archiveProcessing)
+    {
+        if (!$this->shouldArchiveEcommerceItems($archiveProcessing)) {
+            return false;
+        }
+        $items = array();
+
+        $dimensionsToQuery = $this->dimensions;
+        $dimensionsToQuery['idaction_category2'] = 'AdditionalCategory';
+        $dimensionsToQuery['idaction_category3'] = 'AdditionalCategory';
+        $dimensionsToQuery['idaction_category4'] = 'AdditionalCategory';
+        $dimensionsToQuery['idaction_category5'] = 'AdditionalCategory';
+
+        foreach ($dimensionsToQuery as $dimension => $recordName) {
+            $query = $archiveProcessing->queryEcommerceItems($dimension);
+            if ($query == false) {
+                continue;
+            }
+
+            while ($row = $query->fetch()) {
+                $label = $row['label'];
+                $ecommerceType = $row['ecommerceType'];
+
+                if (empty($label)) {
+                    // idaction==0 case:
+                    // If we are querying any optional category, we do not include idaction=0
+                    // Otherwise we over-report in the Product Categories report
+                    if ($recordName == 'AdditionalCategory') {
+                        continue;
+                    }
+                    // Product Name/Category not defined"
+                    if (class_exists('Piwik_CustomVariables')) {
+                        $label = Piwik_CustomVariables_Archiving::LABEL_CUSTOM_VALUE_NOT_DEFINED;
+                    } else {
+                        $label = "Value not defined";
+                    }
+                }
+                // For carts, idorder = 0. To count abandoned carts, we must count visits with an abandoned cart
+                if ($ecommerceType == Piwik_Tracker_GoalManager::IDGOAL_CART) {
+                    $row[Piwik_Archive::INDEX_ECOMMERCE_ORDERS] = $row[Piwik_Archive::INDEX_NB_VISITS];
+                }
+                unset($row[Piwik_Archive::INDEX_NB_VISITS]);
+                unset($row['label']);
+                unset($row['ecommerceType']);
+
+                $columnsToRound = array(
+                    Piwik_Archive::INDEX_ECOMMERCE_ITEM_REVENUE,
+                    Piwik_Archive::INDEX_ECOMMERCE_ITEM_QUANTITY,
+                    Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE,
+                    Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED,
+                );
+                foreach ($columnsToRound as $column) {
+                    if (isset($row[$column])
+                        && $row[$column] == round($row[$column])
+                    ) {
+                        $row[$column] = round($row[$column]);
+                    }
+                }
+                $items[$dimension][$ecommerceType][$label] = $row;
+            }
+        }
+
+        foreach ($this->dimensions as $dimension => $recordName) {
+            foreach (array(Piwik_Tracker_GoalManager::IDGOAL_CART, Piwik_Tracker_GoalManager::IDGOAL_ORDER) as $ecommerceType) {
+                if (!isset($items[$dimension][$ecommerceType])) {
+                    continue;
+                }
+                $recordNameInsert = $recordName;
+                if ($ecommerceType == Piwik_Tracker_GoalManager::IDGOAL_CART) {
+                    $recordNameInsert = self::getItemRecordNameAbandonedCart($recordName);
+                }
+                $table = $archiveProcessing->getDataTableFromArray($items[$dimension][$ecommerceType]);
+
+                // For "category" report, we aggregate all 5 category queries into one datatable
+                if ($dimension == 'idaction_category') {
+                    foreach (array('idaction_category2', 'idaction_category3', 'idaction_category4', 'idaction_category5') as $categoryToSum) {
+                        if (!empty($items[$categoryToSum][$ecommerceType])) {
+                            $tableToSum = $archiveProcessing->getDataTableFromArray($items[$categoryToSum][$ecommerceType]);
+                            $table->addDataTable($tableToSum);
+                        }
+                    }
+                }
+                $archiveProcessing->insertBlobRecord($recordNameInsert, $table->getSerialized());
+            }
+        }
+    }
+
+    protected function shouldArchiveEcommerceItems($archiveProcessing)
+    {
+        // Per item doesn't support segment
+        // Also, when querying Goal metrics for visitorType==returning, we wouldnt want to trigger an extra request
+        // event if it did support segment
+        // (if this is implented, we should have shouldProcessReportsForPlugin() support partial archiving based on which metric is requested)
+        if (!$archiveProcessing->getSegment()->isEmpty()) {
+            return false;
+        }
+        return true;
+    }
+
+    static public function getItemRecordNameAbandonedCart($recordName)
+    {
+        return $recordName . '_Cart';
+    }
+
+    /**
+     * @param $archiveProcessing
+     */
+    public function archivePeriod($archiveProcessing)
+    {
+        /*
+         * Archive Ecommerce Items
+         */
+        if ($this->shouldArchiveEcommerceItems($archiveProcessing)) {
+            $dataTableToSum = $this->dimensions;
+            foreach ($this->dimensions as $recordName) {
+                $dataTableToSum[] = self::getItemRecordNameAbandonedCart($recordName);
+            }
+            $archiveProcessing->archiveDataTable($dataTableToSum);
+        }
+
+        /*
+         *  Archive General Goal metrics
+         */
+        $goalIdsToSum = Piwik_Tracker_GoalManager::getGoalIds($archiveProcessing->idsite);
+
+        //Ecommerce
+        $goalIdsToSum[] = Piwik_Tracker_GoalManager::IDGOAL_ORDER;
+        $goalIdsToSum[] = Piwik_Tracker_GoalManager::IDGOAL_CART; //bug here if idgoal=1
+        // Overall goal metrics
+        $goalIdsToSum[] = false;
+
+        $fieldsToSum = array();
+        foreach ($goalIdsToSum as $goalId) {
+            $metricsToSum = Piwik_Goals::getGoalColumns($goalId);
+            unset($metricsToSum[array_search('conversion_rate', $metricsToSum)]);
+            foreach ($metricsToSum as $metricName) {
+                $fieldsToSum[] = self::getRecordName($metricName, $goalId);
+            }
+        }
+        $records = $archiveProcessing->archiveNumericValuesSum($fieldsToSum);
+
+        // also recording conversion_rate for each goal
+        foreach ($goalIdsToSum as $goalId) {
+            $nb_conversions = $records[self::getRecordName('nb_visits_converted', $goalId)];
+            $conversion_rate = $this->getConversionRate($nb_conversions, $archiveProcessing);
+            $archiveProcessing->insertNumericRecord(self::getRecordName('conversion_rate', $goalId), $conversion_rate);
+
+            // sum up the visits to conversion data table & the days to conversion data table
+            $archiveProcessing->archiveDataTable(array(
+                                                      self::getRecordName(self::VISITS_UNTIL_RECORD_NAME, $goalId),
+                                                      self::getRecordName(self::DAYS_UNTIL_CONV_RECORD_NAME, $goalId)));
+        }
+
+        // sum up goal overview reports
+        $archiveProcessing->archiveDataTable(array(
+                                                  self::getRecordName(self::VISITS_UNTIL_RECORD_NAME),
+                                                  self::getRecordName(self::DAYS_UNTIL_CONV_RECORD_NAME)));
+    }
+
+}
\ No newline at end of file
diff --git a/plugins/Goals/Controller.php b/plugins/Goals/Controller.php
index 15eb85c345034a5c4ca15bed0f2d795df618af5f..7f374e40f3d3240a666807d267f1fb08c5664934 100644
--- a/plugins/Goals/Controller.php
+++ b/plugins/Goals/Controller.php
@@ -361,7 +361,7 @@ class Piwik_Goals_Controller extends Piwik_Controller
 
         $keywordNotDefinedString = '';
         if (Piwik_PluginsManager::getInstance()->isPluginActivated('Referers')) {
-            $keywordNotDefinedString = Piwik_Referers::getKeywordNotDefinedString();
+            $keywordNotDefinedString = Piwik_Referers_API::getKeywordNotDefinedString();
             $topDimensionsToLoad += array(
                 'keyword' => 'Referers.getKeywords',
                 'website' => 'Referers.getWebsites',
diff --git a/plugins/Live/Visitor.php b/plugins/Live/Visitor.php
index 8a8c9840214fb9e9cc92d7e4e1cc3aaa670dc19e..5228e18aa86544874f48dac27febf8a631c1e0a7 100644
--- a/plugins/Live/Visitor.php
+++ b/plugins/Live/Visitor.php
@@ -344,7 +344,7 @@ class Piwik_Live_Visitor
         if (Piwik_PluginsManager::getInstance()->isPluginActivated('Referers')
             && $this->getRefererType() == 'search'
         ) {
-            $keyword = Piwik_Referers::getCleanKeyword($keyword);
+            $keyword = Piwik_Referers_API::getCleanKeyword($keyword);
         }
         return urldecode($keyword);
     }
@@ -353,7 +353,7 @@ class Piwik_Live_Visitor
     {
         if ($this->getRefererType() == 'search') {
             if (Piwik_PluginsManager::getInstance()->isPluginActivated('Referers')
-                && $this->details['referer_keyword'] == Piwik_Referers::LABEL_KEYWORD_NOT_DEFINED
+                && $this->details['referer_keyword'] == Piwik_Referers_API::LABEL_KEYWORD_NOT_DEFINED
             ) {
                 return 'http://piwik.org/faq/general/#faq_144';
             } // Case URL is google.XX/url.... then we rewrite to the search result page url
diff --git a/plugins/Provider/Archiving.php b/plugins/Provider/Archiving.php
new file mode 100644
index 0000000000000000000000000000000000000000..c0228e788fb540c5f45b84a59b613f386c000417
--- /dev/null
+++ b/plugins/Provider/Archiving.php
@@ -0,0 +1,30 @@
+<?php
+
+class Piwik_Provider_Archiving {
+
+
+    /**
+     * @param $archiveProcessing
+     */
+    public function archiveDay($archiveProcessing)
+    {
+        $recordName = 'Provider_hostnameExt';
+        $labelSQL = "log_visit.location_provider";
+        $metricsByProvider = $archiveProcessing->getArrayInterestForLabel($labelSQL);
+        $tableProvider = $archiveProcessing->getDataTableFromArray($metricsByProvider);
+        $columnToSortByBeforeTruncation = Piwik_Archive::INDEX_NB_VISITS;
+        $maximumRowsInDataTable = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_standard'];
+        $archiveProcessing->insertBlobRecord($recordName, $tableProvider->getSerialized($maximumRowsInDataTable, null, $columnToSortByBeforeTruncation));
+    }
+
+    /**
+     * @param $archiveProcessing
+     */
+    public function archivePeriod($archiveProcessing)
+    {
+        $maximumRowsInDataTable = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_standard'];
+        $dataTableToSum = array('Provider_hostnameExt');
+        $archiveProcessing->archiveDataTable($dataTableToSum, null, $maximumRowsInDataTable);
+    }
+
+}
\ No newline at end of file
diff --git a/plugins/Provider/Provider.php b/plugins/Provider/Provider.php
index 67a1d35371e89c1f2d470539f659e99e04d1512b..87c7de7efb36788ee0b3d34328dd4e8e3428b66f 100644
--- a/plugins/Provider/Provider.php
+++ b/plugins/Provider/Provider.php
@@ -114,42 +114,6 @@ class Piwik_Provider extends Piwik_Plugin
         Piwik_AddAction('template_footerUserCountry', array('Piwik_Provider', 'footerUserCountry'));
     }
 
-    /**
-     * @param Piwik_Event_Notification $notification  notification object
-     * @return mixed
-     */
-    function archivePeriod($notification)
-    {
-        $maximumRowsInDataTable = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_standard'];
-        $archiveProcessing = $notification->getNotificationObject();
-
-        if (!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return;
-
-        $dataTableToSum = array('Provider_hostnameExt');
-        $archiveProcessing->archiveDataTable($dataTableToSum, null, $maximumRowsInDataTable);
-    }
-
-    /**
-     * Daily archive: processes the report Visits by Provider
-     *
-     * @param Piwik_Event_Notification $notification  notification object
-     */
-    function archiveDay($notification)
-    {
-        $archiveProcessing = $notification->getNotificationObject();
-
-        if (!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return;
-
-        $recordName = 'Provider_hostnameExt';
-        $labelSQL = "log_visit.location_provider";
-        $metricsByProvider = $archiveProcessing->getArrayInterestForLabel($labelSQL);
-        $tableProvider = $archiveProcessing->getDataTableFromArray($metricsByProvider);
-        $columnToSortByBeforeTruncation = Piwik_Archive::INDEX_NB_VISITS;
-        $maximumRowsInDataTable = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_standard'];
-        $archiveProcessing->insertBlobRecord($recordName, $tableProvider->getSerialized($maximumRowsInDataTable, null, $columnToSortByBeforeTruncation));
-        destroy($tableProvider);
-    }
-
     /**
      * Logs the provider in the log_visit table
      *
@@ -250,4 +214,38 @@ class Piwik_Provider extends Piwik_Plugin
         $out .= '</div>';
     }
 
+    /**
+     * Daily archive: processes the report Visits by Provider
+     *
+     * @param Piwik_Event_Notification $notification  notification object
+     */
+    function archiveDay($notification)
+    {
+        $archiveProcessing = $notification->getNotificationObject();
+
+        if (!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) {
+            return;
+        }
+
+        $archiving = new Piwik_Provider_Archiving();
+        $archiving->archiveDay($archiveProcessing);
+    }
+
+    /**
+     * @param Piwik_Event_Notification $notification  notification object
+     * @return mixed
+     */
+    function archivePeriod($notification)
+    {
+        $archiveProcessing = $notification->getNotificationObject();
+
+        if (!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) {
+            return;
+        }
+
+        $archiving = new Piwik_Provider_Archiving();
+        $archiving->archivePeriod($archiveProcessing);
+    }
+
+
 }
diff --git a/plugins/Referers/API.php b/plugins/Referers/API.php
index 6516228fd6cbc14b9de3c48e5be8fc8fb0a15247..327fa59c2cb936e54bb109f65ec256609c28a5a3 100644
--- a/plugins/Referers/API.php
+++ b/plugins/Referers/API.php
@@ -138,10 +138,30 @@ class Piwik_Referers_API
 
     protected function handleKeywordNotDefined($dataTable)
     {
-        $dataTable->queueFilter('ColumnCallbackReplace', array('label', array('Piwik_Referers', 'getCleanKeyword')));
+        $dataTable->queueFilter('ColumnCallbackReplace', array('label', array('Piwik_Referers_API', 'getCleanKeyword')));
         return $dataTable;
     }
 
+    const LABEL_KEYWORD_NOT_DEFINED = "";
+
+    /**
+     * @ignore
+     */
+    static public function getKeywordNotDefinedString()
+    {
+        return Piwik_Translate('General_NotDefined', Piwik_Translate('Referers_ColumnKeyword'));
+    }
+
+    /**
+     * @ignore
+     */
+    static public function getCleanKeyword($label)
+    {
+        return $label == self::LABEL_KEYWORD_NOT_DEFINED
+            ? self::getKeywordNotDefinedString()
+            : $label;
+    }
+
     public function getKeywordsForPageUrl($idSite, $period, $date, $url)
     {
         // Fetch the Top keywords for this page
diff --git a/plugins/Referers/Archiving.php b/plugins/Referers/Archiving.php
new file mode 100644
index 0000000000000000000000000000000000000000..0fca433ba3b0aa1cf19b4e78ab1f6bfb8e8a4e66
--- /dev/null
+++ b/plugins/Referers/Archiving.php
@@ -0,0 +1,346 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ * @category Piwik_Plugins
+ * @package Piwik_Referers
+ */
+
+class Piwik_Referers_Archiving
+{
+    protected $columnToSortByBeforeTruncation;
+    protected $maximumRowsInDataTableLevelZero;
+    protected $maximumRowsInSubDataTable;
+
+    function __construct()
+    {
+        $this->columnToSortByBeforeTruncation = Piwik_Archive::INDEX_NB_VISITS;
+        $this->maximumRowsInDataTableLevelZero = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_referers'];
+        $this->maximumRowsInSubDataTable = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_subtable_referers'];
+    }
+
+    /**
+     * @param $archiveProcessing
+     */
+    public function archiveDay($archiveProcessing)
+    {
+        $query = $archiveProcessing->queryVisitsByDimension(array("referer_type", "referer_name", "referer_keyword", "referer_url"));
+        $this->aggregateFromVisits($archiveProcessing, $query);
+
+        $query = $archiveProcessing->queryConversionsByDimension(array("referer_type", "referer_name", "referer_keyword"));
+        $this->aggregateFromConversions($archiveProcessing, $query);
+
+        Piwik_PostEvent('Referers.archiveDay', $this);
+        $this->archiveDayRecordInDatabase($archiveProcessing);
+    }
+
+    /**
+     * @param Piwik_ArchiveProcessing_Day $archiveProcessing
+     * @param $query
+     * @throws Exception
+     */
+    protected function aggregateFromVisits(Piwik_ArchiveProcessing_Day $archiveProcessing, $query)
+    {
+        $this->metricsBySearchEngine =
+        $this->metricsByKeyword =
+        $this->metricsBySearchEngineAndKeyword =
+        $this->metricsByKeywordAndSearchEngine =
+        $this->metricsByWebsite =
+        $this->metricsByWebsiteAndUrl =
+        $this->metricsByCampaignAndKeyword =
+        $this->metricsByCampaign =
+        $this->metricsByType =
+        $this->distinctUrls = array();
+        while ($row = $query->fetch()) {
+            $this->makeRefererTypeNonEmpty($row);
+            $this->aggregateVisit($archiveProcessing, $row);
+            $this->aggregateVisitByType($archiveProcessing, $row);
+
+        }
+    }
+
+    protected function makeRefererTypeNonEmpty(&$row)
+    {
+        if (empty($row['referer_type'])) {
+            $row['referer_type'] = Piwik_Common::REFERER_TYPE_DIRECT_ENTRY;
+        }
+    }
+
+    protected function aggregateVisit(Piwik_ArchiveProcessing_Day $archiveProcessing, $row)
+    {
+        switch ($row['referer_type']) {
+            case Piwik_Common::REFERER_TYPE_SEARCH_ENGINE:
+                $this->aggregateVisitBySearchEngine($archiveProcessing, $row);
+                break;
+
+            case Piwik_Common::REFERER_TYPE_WEBSITE:
+                $this->aggregateVisitByWebsite($archiveProcessing, $row);
+                break;
+
+            case Piwik_Common::REFERER_TYPE_CAMPAIGN:
+                $this->aggregateVisitByCampaign($archiveProcessing, $row);
+                break;
+
+            case Piwik_Common::REFERER_TYPE_DIRECT_ENTRY:
+                // direct entry are aggregated below in $this->metricsByType array
+                break;
+
+            default:
+                throw new Exception("Non expected referer_type = " . $row['referer_type']);
+                break;
+        }
+    }
+
+    protected function aggregateVisitBySearchEngine(Piwik_ArchiveProcessing_Day $archiveProcessing, $row)
+    {
+        if (empty($row['referer_keyword'])) {
+            $row['referer_keyword'] = Piwik_Referers_API::LABEL_KEYWORD_NOT_DEFINED;
+        }
+        if (!isset($this->metricsBySearchEngine[$row['referer_name']])) {
+            $this->metricsBySearchEngine[$row['referer_name']] = $archiveProcessing->makeEmptyRow();
+        }
+        if (!isset($this->metricsByKeyword[$row['referer_keyword']])) {
+            $this->metricsByKeyword[$row['referer_keyword']] = $archiveProcessing->makeEmptyRow();
+        }
+        if (!isset($this->metricsBySearchEngineAndKeyword[$row['referer_name']][$row['referer_keyword']])) {
+            $this->metricsBySearchEngineAndKeyword[$row['referer_name']][$row['referer_keyword']] = $archiveProcessing->makeEmptyRow();
+        }
+        if (!isset($this->metricsByKeywordAndSearchEngine[$row['referer_keyword']][$row['referer_name']])) {
+            $this->metricsByKeywordAndSearchEngine[$row['referer_keyword']][$row['referer_name']] = $archiveProcessing->makeEmptyRow();
+        }
+
+        $archiveProcessing->sumMetrics($row, $this->metricsBySearchEngine[$row['referer_name']]);
+        $archiveProcessing->sumMetrics($row, $this->metricsByKeyword[$row['referer_keyword']]);
+        $archiveProcessing->sumMetrics($row, $this->metricsBySearchEngineAndKeyword[$row['referer_name']][$row['referer_keyword']]);
+        $archiveProcessing->sumMetrics($row, $this->metricsByKeywordAndSearchEngine[$row['referer_keyword']][$row['referer_name']]);
+    }
+
+    protected function aggregateVisitByWebsite(Piwik_ArchiveProcessing_Day $archiveProcessing, $row)
+    {
+        if (!isset($this->metricsByWebsite[$row['referer_name']])) {
+            $this->metricsByWebsite[$row['referer_name']] = $archiveProcessing->makeEmptyRow();
+        }
+        $archiveProcessing->sumMetrics($row, $this->metricsByWebsite[$row['referer_name']]);
+
+        if (!isset($this->metricsByWebsiteAndUrl[$row['referer_name']][$row['referer_url']])) {
+            $this->metricsByWebsiteAndUrl[$row['referer_name']][$row['referer_url']] = $archiveProcessing->makeEmptyRow();
+        }
+        $archiveProcessing->sumMetrics($row, $this->metricsByWebsiteAndUrl[$row['referer_name']][$row['referer_url']]);
+
+        $urlHash = substr(md5($row['referer_url']), 0, 10);
+        if (!isset($this->distinctUrls[$urlHash])) {
+            $this->distinctUrls[$urlHash] = true;
+        }
+    }
+
+    protected function aggregateVisitByCampaign(Piwik_ArchiveProcessing_Day $archiveProcessing, $row)
+    {
+        if (!empty($row['referer_keyword'])) {
+            if (!isset($this->metricsByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']])) {
+                $this->metricsByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']] = $archiveProcessing->makeEmptyRow();
+            }
+            $archiveProcessing->sumMetrics($row, $this->metricsByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']]);
+        }
+        if (!isset($this->metricsByCampaign[$row['referer_name']])) {
+            $this->metricsByCampaign[$row['referer_name']] = $archiveProcessing->makeEmptyRow();
+        }
+        $archiveProcessing->sumMetrics($row, $this->metricsByCampaign[$row['referer_name']]);
+    }
+
+    protected function aggregateVisitByType(Piwik_ArchiveProcessing_Day $archiveProcessing, $row)
+    {
+        if (!isset($this->metricsByType[$row['referer_type']])) {
+            $this->metricsByType[$row['referer_type']] = $archiveProcessing->makeEmptyRow();
+        }
+        $archiveProcessing->sumMetrics($row, $this->metricsByType[$row['referer_type']]);
+    }
+
+    protected function aggregateFromConversions($archiveProcessing, $query)
+    {
+        if ($query === false) {
+            return;
+        }
+        while ($row = $query->fetch()) {
+            $this->makeRefererTypeNonEmpty($row);
+
+            $skipAggregateByType = $this->aggregateConversion($archiveProcessing, $row);
+            if (!$skipAggregateByType) {
+                $this->aggregateConversionByType($archiveProcessing, $row);
+            }
+        }
+
+        $archiveProcessing->enrichConversionsByLabelArray($this->metricsByType);
+        $archiveProcessing->enrichConversionsByLabelArray($this->metricsBySearchEngine);
+        $archiveProcessing->enrichConversionsByLabelArray($this->metricsByKeyword);
+        $archiveProcessing->enrichConversionsByLabelArray($this->metricsByWebsite);
+        $archiveProcessing->enrichConversionsByLabelArray($this->metricsByCampaign);
+        $archiveProcessing->enrichConversionsByLabelArrayHasTwoLevels($this->metricsByCampaignAndKeyword);
+    }
+
+    protected function aggregateConversion($archiveProcessing, $row)
+    {
+        $skipAggregateByType = false;
+        switch ($row['referer_type']) {
+            case Piwik_Common::REFERER_TYPE_SEARCH_ENGINE:
+                $this->aggregateConversionBySearchEngine($archiveProcessing, $row);
+                break;
+
+            case Piwik_Common::REFERER_TYPE_WEBSITE:
+                $this->aggregateConversionByWebsite($archiveProcessing, $row);
+                break;
+
+            case Piwik_Common::REFERER_TYPE_CAMPAIGN:
+                $this->aggregateConversionByCampaign($archiveProcessing, $row);
+                break;
+
+            case Piwik_Common::REFERER_TYPE_DIRECT_ENTRY:
+                // Direct entry, no sub dimension
+                break;
+
+            default:
+                // The referer type is user submitted for goal conversions, we ignore any malformed value
+                // Continue to the next while iteration
+                $skipAggregateByType = true;
+                break;
+        }
+        return $skipAggregateByType;
+    }
+
+    protected function aggregateConversionBySearchEngine($archiveProcessing, $row)
+    {
+        if (empty($row['referer_keyword'])) {
+            $row['referer_keyword'] = Piwik_Referers_API::LABEL_KEYWORD_NOT_DEFINED;
+        }
+        if (!isset($this->metricsBySearchEngine[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) {
+            $this->metricsBySearchEngine[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->makeEmptyGoalRow($row['idgoal']);
+        }
+        if (!isset($this->metricsByKeyword[$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) {
+            $this->metricsByKeyword[$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->makeEmptyGoalRow($row['idgoal']);
+        }
+
+        $archiveProcessing->sumGoalMetrics($row, $this->metricsBySearchEngine[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]);
+        $archiveProcessing->sumGoalMetrics($row, $this->metricsByKeyword[$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]);
+    }
+
+    protected function aggregateConversionByWebsite($archiveProcessing, $row)
+    {
+        if (!isset($this->metricsByWebsite[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) {
+            $this->metricsByWebsite[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->makeEmptyGoalRow($row['idgoal']);
+        }
+        $archiveProcessing->sumGoalMetrics($row, $this->metricsByWebsite[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]);
+    }
+
+    protected function aggregateConversionByCampaign($archiveProcessing, $row)
+    {
+        if (!empty($row['referer_keyword'])) {
+            if (!isset($this->metricsByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) {
+                $this->metricsByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->makeEmptyGoalRow($row['idgoal']);
+            }
+            $archiveProcessing->sumGoalMetrics($row, $this->metricsByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]);
+        }
+        if (!isset($this->metricsByCampaign[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) {
+            $this->metricsByCampaign[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->makeEmptyGoalRow($row['idgoal']);
+        }
+        $archiveProcessing->sumGoalMetrics($row, $this->metricsByCampaign[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]);
+    }
+
+    protected function aggregateConversionByType($archiveProcessing, $row)
+    {
+        if (!isset($this->metricsByType[$row['referer_type']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) {
+            $this->metricsByType[$row['referer_type']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->makeEmptyGoalRow($row['idgoal']);
+        }
+        $archiveProcessing->sumGoalMetrics($row, $this->metricsByType[$row['referer_type']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]);
+    }
+
+    /**
+     * Records the daily stats (numeric or datatable blob) into the archive tables.
+     *
+     * @param Piwik_ArchiveProcessing $archiveProcessing
+     * @return void
+     */
+    protected function archiveDayRecordInDatabase($archiveProcessing)
+    {
+        $numericRecords = array(
+            'Referers_distinctSearchEngines' => count($this->metricsBySearchEngineAndKeyword),
+            'Referers_distinctKeywords'      => count($this->metricsByKeywordAndSearchEngine),
+            'Referers_distinctCampaigns'     => count($this->metricsByCampaign),
+            'Referers_distinctWebsites'      => count($this->metricsByWebsite),
+            'Referers_distinctWebsitesUrls'  => count($this->distinctUrls),
+        );
+
+        foreach ($numericRecords as $name => $value) {
+            $archiveProcessing->insertNumericRecord($name, $value);
+        }
+
+        $dataTable = $archiveProcessing->getDataTableSerialized($this->metricsByType);
+        $archiveProcessing->insertBlobRecord('Referers_type', $dataTable);
+        destroy($dataTable);
+
+        $blobRecords = array(
+            'Referers_keywordBySearchEngine' => $archiveProcessing->getDataTableWithSubtablesFromArraysIndexedByLabel($this->metricsBySearchEngineAndKeyword, $this->metricsBySearchEngine),
+            'Referers_searchEngineByKeyword' => $archiveProcessing->getDataTableWithSubtablesFromArraysIndexedByLabel($this->metricsByKeywordAndSearchEngine, $this->metricsByKeyword),
+            'Referers_keywordByCampaign'     => $archiveProcessing->getDataTableWithSubtablesFromArraysIndexedByLabel($this->metricsByCampaignAndKeyword, $this->metricsByCampaign),
+            'Referers_urlByWebsite'          => $archiveProcessing->getDataTableWithSubtablesFromArraysIndexedByLabel($this->metricsByWebsiteAndUrl, $this->metricsByWebsite),
+        );
+        foreach ($blobRecords as $recordName => $table) {
+            $blob = $table->getSerialized($this->maximumRowsInDataTableLevelZero, $this->maximumRowsInSubDataTable, $this->columnToSortByBeforeTruncation);
+            $archiveProcessing->insertBlobRecord($recordName, $blob);
+            destroy($table);
+        }
+    }
+
+    /**
+     * @param $archiveProcessing
+     */
+    public function archivePeriod($archiveProcessing)
+    {
+        $dataTableToSum = array(
+            'Referers_type',
+            'Referers_keywordBySearchEngine',
+            'Referers_searchEngineByKeyword',
+            'Referers_keywordByCampaign',
+            'Referers_urlByWebsite',
+        );
+        $nameToCount = $archiveProcessing->archiveDataTable($dataTableToSum, null, $this->maximumRowsInDataTableLevelZero, $this->maximumRowsInSubDataTable, $this->columnToSortByBeforeTruncation);
+
+        $mappingFromArchiveName = array(
+            'Referers_distinctSearchEngines' =>
+            array('typeCountToUse' => 'level0',
+                  'nameTableToUse' => 'Referers_keywordBySearchEngine',
+            ),
+            'Referers_distinctKeywords'      =>
+            array('typeCountToUse' => 'level0',
+                  'nameTableToUse' => 'Referers_searchEngineByKeyword',
+            ),
+            'Referers_distinctCampaigns'     =>
+            array('typeCountToUse' => 'level0',
+                  'nameTableToUse' => 'Referers_keywordByCampaign',
+            ),
+            'Referers_distinctWebsites'      =>
+            array('typeCountToUse' => 'level0',
+                  'nameTableToUse' => 'Referers_urlByWebsite',
+            ),
+            'Referers_distinctWebsitesUrls'  =>
+            array('typeCountToUse' => 'recursive',
+                  'nameTableToUse' => 'Referers_urlByWebsite',
+            ),
+        );
+
+        foreach ($mappingFromArchiveName as $name => $infoMapping) {
+            $typeCountToUse = $infoMapping['typeCountToUse'];
+            $nameTableToUse = $infoMapping['nameTableToUse'];
+
+            if ($typeCountToUse == 'recursive') {
+
+                $countValue = $nameToCount[$nameTableToUse]['recursive']
+                    - $nameToCount[$nameTableToUse]['level0'];
+            } else {
+                $countValue = $nameToCount[$nameTableToUse]['level0'];
+            }
+            $archiveProcessing->insertNumericRecord($name, $countValue);
+        }
+    }
+}
\ No newline at end of file
diff --git a/plugins/Referers/Referers.php b/plugins/Referers/Referers.php
index 2c36a17d5a7f797054646f780b6dca238050869f..814e78f19b3f73c384ea4df5c2aea371053a60a1 100644
--- a/plugins/Referers/Referers.php
+++ b/plugins/Referers/Referers.php
@@ -19,21 +19,14 @@ require_once PIWIK_INCLUDE_PATH . '/plugins/Referers/functions.php';
  */
 class Piwik_Referers extends Piwik_Plugin
 {
-    public $archiveProcessing;
-    protected $columnToSortByBeforeTruncation;
-    protected $maximumRowsInDataTableLevelZero;
-    protected $maximumRowsInSubDataTable;
-
     public function getInformation()
     {
-        $info = array(
+        return array(
             'description'     => Piwik_Translate('Referers_PluginDescription'),
             'author'          => 'Piwik',
             'author_homepage' => 'http://piwik.org/',
             'version'         => Piwik_Version::VERSION,
         );
-
-        return $info;
     }
 
     function getListHooksRegistered()
@@ -289,86 +282,6 @@ class Piwik_Referers extends Piwik_Plugin
                                                ));
     }
 
-    function __construct()
-    {
-        $this->columnToSortByBeforeTruncation = Piwik_Archive::INDEX_NB_VISITS;
-        $this->maximumRowsInDataTableLevelZero = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_referers'];
-        $this->maximumRowsInSubDataTable = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_subtable_referers'];
-    }
-
-    /**
-     * Period archiving: sums up daily stats and sums report tables,
-     * making sure that tables are still truncated.
-     *
-     * @param Piwik_Event_Notification $notification  notification object
-     * @return void
-     */
-    function archivePeriod($notification)
-    {
-        $archiveProcessing = $notification->getNotificationObject();
-
-        if (!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return;
-
-        $dataTableToSum = array(
-            'Referers_type',
-            'Referers_keywordBySearchEngine',
-            'Referers_searchEngineByKeyword',
-            'Referers_keywordByCampaign',
-            'Referers_urlByWebsite',
-        );
-        $nameToCount = $archiveProcessing->archiveDataTable($dataTableToSum, null, $this->maximumRowsInDataTableLevelZero, $this->maximumRowsInSubDataTable, $this->columnToSortByBeforeTruncation);
-
-        $mappingFromArchiveName = array(
-            'Referers_distinctSearchEngines' =>
-            array('typeCountToUse' => 'level0',
-                  'nameTableToUse' => 'Referers_keywordBySearchEngine',
-            ),
-            'Referers_distinctKeywords'      =>
-            array('typeCountToUse' => 'level0',
-                  'nameTableToUse' => 'Referers_searchEngineByKeyword',
-            ),
-            'Referers_distinctCampaigns'     =>
-            array('typeCountToUse' => 'level0',
-                  'nameTableToUse' => 'Referers_keywordByCampaign',
-            ),
-            'Referers_distinctWebsites'      =>
-            array('typeCountToUse' => 'level0',
-                  'nameTableToUse' => 'Referers_urlByWebsite',
-            ),
-            'Referers_distinctWebsitesUrls'  =>
-            array('typeCountToUse' => 'recursive',
-                  'nameTableToUse' => 'Referers_urlByWebsite',
-            ),
-        );
-
-        foreach ($mappingFromArchiveName as $name => $infoMapping) {
-            $typeCountToUse = $infoMapping['typeCountToUse'];
-            $nameTableToUse = $infoMapping['nameTableToUse'];
-
-            if ($typeCountToUse == 'recursive') {
-
-                $countValue = $nameToCount[$nameTableToUse]['recursive']
-                    - $nameToCount[$nameTableToUse]['level0'];
-            } else {
-                $countValue = $nameToCount[$nameTableToUse]['level0'];
-            }
-            $archiveProcessing->insertNumericRecord($name, $countValue);
-        }
-    }
-
-    const LABEL_KEYWORD_NOT_DEFINED = "";
-
-    static public function getKeywordNotDefinedString()
-    {
-        return Piwik_Translate('General_NotDefined', Piwik_Translate('Referers_ColumnKeyword'));
-    }
-
-    static public function getCleanKeyword($label)
-    {
-        return $label == Piwik_Referers::LABEL_KEYWORD_NOT_DEFINED
-            ? self::getKeywordNotDefinedString()
-            : $label;
-    }
 
     /**
      * Hooks on daily archive to trigger various log processing
@@ -381,208 +294,33 @@ class Piwik_Referers extends Piwik_Plugin
         /**
          * @var Piwik_ArchiveProcessing_Day
          */
-        $this->archiveProcessing = $notification->getNotificationObject();
-        if (!$this->archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return;
-
-        $this->archiveDayAggregateVisits($this->archiveProcessing);
-        $this->archiveDayAggregateGoals($this->archiveProcessing);
-        Piwik_PostEvent('Referers.archiveDay', $this);
-        $this->archiveDayRecordInDatabase($this->archiveProcessing);
-        $this->cleanup();
-    }
-
-    protected function cleanup()
-    {
-        destroy($this->metricsBySearchEngine);
-        destroy($this->metricsByKeyword);
-        destroy($this->metricsBySearchEngineAndKeyword);
-        destroy($this->metricsByKeywordAndSearchEngine);
-        destroy($this->metricsByWebsite);
-        destroy($this->metricsByWebsiteAndUrl);
-        destroy($this->metricsByCampaignAndKeyword);
-        destroy($this->metricsByCampaign);
-        destroy($this->metricsByType);
-        destroy($this->distinctUrls);
-    }
-
-    /**
-     * Daily archive: processes all Referers reports, eg. Visits by Keyword,
-     * Visits by websites, etc.
-     *
-     * @param Piwik_ArchiveProcessing $archiveProcessing
-     * @throws Exception
-     * @return void
-     */
-    protected function archiveDayAggregateVisits(Piwik_ArchiveProcessing_Day $archiveProcessing)
-    {
-        $dimension = array("referer_type", "referer_name", "referer_keyword", "referer_url");
-        $query = $archiveProcessing->queryVisitsByDimension($dimension);
-
-        $this->metricsBySearchEngine =
-        $this->metricsByKeyword =
-        $this->metricsBySearchEngineAndKeyword =
-        $this->metricsByKeywordAndSearchEngine =
-        $this->metricsByWebsite =
-        $this->metricsByWebsiteAndUrl =
-        $this->metricsByCampaignAndKeyword =
-        $this->metricsByCampaign =
-        $this->metricsByType =
-        $this->distinctUrls = array();
-        while ($row = $query->fetch()) {
-            if (empty($row['referer_type'])) {
-                $row['referer_type'] = Piwik_Common::REFERER_TYPE_DIRECT_ENTRY;
-            } else {
-                switch ($row['referer_type']) {
-                    case Piwik_Common::REFERER_TYPE_SEARCH_ENGINE:
-                        if (empty($row['referer_keyword'])) {
-                            $row['referer_keyword'] = self::LABEL_KEYWORD_NOT_DEFINED;
-                        }
-                        if (!isset($this->metricsBySearchEngine[$row['referer_name']])) $this->metricsBySearchEngine[$row['referer_name']] = $archiveProcessing->makeEmptyRow();
-                        if (!isset($this->metricsByKeyword[$row['referer_keyword']])) $this->metricsByKeyword[$row['referer_keyword']] = $archiveProcessing->makeEmptyRow();
-                        if (!isset($this->metricsBySearchEngineAndKeyword[$row['referer_name']][$row['referer_keyword']])) $this->metricsBySearchEngineAndKeyword[$row['referer_name']][$row['referer_keyword']] = $archiveProcessing->makeEmptyRow();
-                        if (!isset($this->metricsByKeywordAndSearchEngine[$row['referer_keyword']][$row['referer_name']])) $this->metricsByKeywordAndSearchEngine[$row['referer_keyword']][$row['referer_name']] = $archiveProcessing->makeEmptyRow();
-
-                        $archiveProcessing->sumMetrics($row, $this->metricsBySearchEngine[$row['referer_name']]);
-                        $archiveProcessing->sumMetrics($row, $this->metricsByKeyword[$row['referer_keyword']]);
-                        $archiveProcessing->sumMetrics($row, $this->metricsBySearchEngineAndKeyword[$row['referer_name']][$row['referer_keyword']]);
-                        $archiveProcessing->sumMetrics($row, $this->metricsByKeywordAndSearchEngine[$row['referer_keyword']][$row['referer_name']]);
-                        break;
-
-                    case Piwik_Common::REFERER_TYPE_WEBSITE:
-
-                        if (!isset($this->metricsByWebsite[$row['referer_name']])) $this->metricsByWebsite[$row['referer_name']] = $archiveProcessing->makeEmptyRow();
-                        $archiveProcessing->sumMetrics($row, $this->metricsByWebsite[$row['referer_name']]);
-
-                        if (!isset($this->metricsByWebsiteAndUrl[$row['referer_name']][$row['referer_url']])) $this->metricsByWebsiteAndUrl[$row['referer_name']][$row['referer_url']] = $archiveProcessing->makeEmptyRow();
-                        $archiveProcessing->sumMetrics($row, $this->metricsByWebsiteAndUrl[$row['referer_name']][$row['referer_url']]);
-
-                        if (!isset($this->distinctUrls[$row['referer_url']])) {
-                            $this->distinctUrls[$row['referer_url']] = true;
-                        }
-                        break;
-
-                    case Piwik_Common::REFERER_TYPE_CAMPAIGN:
-                        if (!empty($row['referer_keyword'])) {
-                            if (!isset($this->metricsByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']])) $this->metricsByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']] = $archiveProcessing->makeEmptyRow();
-                            $archiveProcessing->sumMetrics($row, $this->metricsByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']]);
-                        }
-                        if (!isset($this->metricsByCampaign[$row['referer_name']])) $this->metricsByCampaign[$row['referer_name']] = $archiveProcessing->makeEmptyRow();
-                        $archiveProcessing->sumMetrics($row, $this->metricsByCampaign[$row['referer_name']]);
-                        break;
-
-                    case Piwik_Common::REFERER_TYPE_DIRECT_ENTRY:
-                        // direct entry are aggregated below in $this->metricsByType array
-                        break;
-
-                    default:
-                        throw new Exception("Non expected referer_type = " . $row['referer_type']);
-                        break;
-                }
-            }
-            if (!isset($this->metricsByType[$row['referer_type']])) $this->metricsByType[$row['referer_type']] = $archiveProcessing->makeEmptyRow();
-            $archiveProcessing->sumMetrics($row, $this->metricsByType[$row['referer_type']]);
+        $archiveProcessing = $notification->getNotificationObject();
+        if (!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) {
+            return;
         }
-    }
-
-    /**
-     * Daily Goal archiving:  processes reports of Goal conversions by Keyword,
-     * Goal conversions by Referer Websites, etc.
-     *
-     * @param Piwik_ArchiveProcessing $archiveProcessing
-     * @return void
-     */
-    protected function archiveDayAggregateGoals($archiveProcessing)
-    {
-        $query = $archiveProcessing->queryConversionsByDimension(array("referer_type", "referer_name", "referer_keyword"));
-
-        if ($query === false) return;
-        while ($row = $query->fetch()) {
-            if (empty($row['referer_type'])) {
-                $row['referer_type'] = Piwik_Common::REFERER_TYPE_DIRECT_ENTRY;
-            } else {
-                switch ($row['referer_type']) {
-                    case Piwik_Common::REFERER_TYPE_SEARCH_ENGINE:
-                        if (empty($row['referer_keyword'])) {
-                            $row['referer_keyword'] = self::LABEL_KEYWORD_NOT_DEFINED;
-                        }
-                        if (!isset($this->metricsBySearchEngine[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->metricsBySearchEngine[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->makeEmptyGoalRow($row['idgoal']);
-                        if (!isset($this->metricsByKeyword[$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->metricsByKeyword[$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->makeEmptyGoalRow($row['idgoal']);
-
-                        $archiveProcessing->sumGoalMetrics($row, $this->metricsBySearchEngine[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]);
-                        $archiveProcessing->sumGoalMetrics($row, $this->metricsByKeyword[$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]);
-                        break;
 
-                    case Piwik_Common::REFERER_TYPE_WEBSITE:
-                        if (!isset($this->metricsByWebsite[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->metricsByWebsite[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->makeEmptyGoalRow($row['idgoal']);
-                        $archiveProcessing->sumGoalMetrics($row, $this->metricsByWebsite[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]);
-                        break;
-
-                    case Piwik_Common::REFERER_TYPE_CAMPAIGN:
-                        if (!empty($row['referer_keyword'])) {
-                            if (!isset($this->metricsByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->metricsByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->makeEmptyGoalRow($row['idgoal']);
-                            $archiveProcessing->sumGoalMetrics($row, $this->metricsByCampaignAndKeyword[$row['referer_name']][$row['referer_keyword']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]);
-                        }
-                        if (!isset($this->metricsByCampaign[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->metricsByCampaign[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->makeEmptyGoalRow($row['idgoal']);
-                        $archiveProcessing->sumGoalMetrics($row, $this->metricsByCampaign[$row['referer_name']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]);
-                        break;
+        $archiving = new Piwik_Referers_Archiving();
+        $archiving->archiveDay($archiveProcessing);
+    }
 
-                    case Piwik_Common::REFERER_TYPE_DIRECT_ENTRY:
-                        // Direct entry, no sub dimension
-                        break;
 
-                    default:
-                        // The referer type is user submitted for goal conversions, we ignore any malformed value
-                        // Continue to the next while iteration
-                        continue 2;
-                        break;
-                }
-            }
-            if (!isset($this->metricsByType[$row['referer_type']][Piwik_Archive::INDEX_GOALS][$row['idgoal']])) $this->metricsByType[$row['referer_type']][Piwik_Archive::INDEX_GOALS][$row['idgoal']] = $archiveProcessing->makeEmptyGoalRow($row['idgoal']);
-            $archiveProcessing->sumGoalMetrics($row, $this->metricsByType[$row['referer_type']][Piwik_Archive::INDEX_GOALS][$row['idgoal']]);
-        }
-
-        $archiveProcessing->enrichConversionsByLabelArray($this->metricsByType);
-        $archiveProcessing->enrichConversionsByLabelArray($this->metricsBySearchEngine);
-        $archiveProcessing->enrichConversionsByLabelArray($this->metricsByKeyword);
-        $archiveProcessing->enrichConversionsByLabelArray($this->metricsByWebsite);
-        $archiveProcessing->enrichConversionsByLabelArray($this->metricsByCampaign);
-        $archiveProcessing->enrichConversionsByLabelArrayHasTwoLevels($this->metricsByCampaignAndKeyword);
-    }
 
     /**
-     * Records the daily stats (numeric or datatable blob) into the archive tables.
+     * Period archiving: sums up daily stats and sums report tables,
+     * making sure that tables are still truncated.
      *
-     * @param Piwik_ArchiveProcessing $archiveProcessing
+     * @param Piwik_Event_Notification $notification  notification object
      * @return void
      */
-    protected function archiveDayRecordInDatabase($archiveProcessing)
+    function archivePeriod($notification)
     {
-        $numericRecords = array(
-            'Referers_distinctSearchEngines' => count($this->metricsBySearchEngineAndKeyword),
-            'Referers_distinctKeywords'      => count($this->metricsByKeywordAndSearchEngine),
-            'Referers_distinctCampaigns'     => count($this->metricsByCampaign),
-            'Referers_distinctWebsites'      => count($this->metricsByWebsite),
-            'Referers_distinctWebsitesUrls'  => count($this->distinctUrls),
-        );
+        $archiveProcessing = $notification->getNotificationObject();
 
-        foreach ($numericRecords as $name => $value) {
-            $archiveProcessing->insertNumericRecord($name, $value);
+        if (!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) {
+            return;
         }
 
-        $dataTable = $archiveProcessing->getDataTableSerialized($this->metricsByType);
-        $archiveProcessing->insertBlobRecord('Referers_type', $dataTable);
-        destroy($dataTable);
-
-        $blobRecords = array(
-            'Referers_keywordBySearchEngine' => $archiveProcessing->getDataTableWithSubtablesFromArraysIndexedByLabel($this->metricsBySearchEngineAndKeyword, $this->metricsBySearchEngine),
-            'Referers_searchEngineByKeyword' => $archiveProcessing->getDataTableWithSubtablesFromArraysIndexedByLabel($this->metricsByKeywordAndSearchEngine, $this->metricsByKeyword),
-            'Referers_keywordByCampaign'     => $archiveProcessing->getDataTableWithSubtablesFromArraysIndexedByLabel($this->metricsByCampaignAndKeyword, $this->metricsByCampaign),
-            'Referers_urlByWebsite'          => $archiveProcessing->getDataTableWithSubtablesFromArraysIndexedByLabel($this->metricsByWebsiteAndUrl, $this->metricsByWebsite),
-        );
-        foreach ($blobRecords as $recordName => $table) {
-            $blob = $table->getSerialized($this->maximumRowsInDataTableLevelZero, $this->maximumRowsInSubDataTable, $this->columnToSortByBeforeTruncation);
-            $archiveProcessing->insertBlobRecord($recordName, $blob);
-            destroy($table);
-        }
+        $archiving = new Piwik_Referers_Archiving();
+        $archiving->archivePeriod($archiveProcessing);
     }
 }
diff --git a/plugins/Referers/functions.php b/plugins/Referers/functions.php
index c7d69386c7fa0a096d85fb5faf514369d1c260a6..46d9e180007fa5fbcb3f421121e728b9a18679e0 100644
--- a/plugins/Referers/functions.php
+++ b/plugins/Referers/functions.php
@@ -178,7 +178,7 @@ function Piwik_getSearchEngineHostPathFromUrl($url)
  */
 function Piwik_getSearchEngineUrlFromUrlAndKeyword($url, $keyword)
 {
-    if ($keyword === Piwik_Referers::LABEL_KEYWORD_NOT_DEFINED) {
+    if ($keyword === Piwik_Referers_API::LABEL_KEYWORD_NOT_DEFINED) {
         return 'http://piwik.org/faq/general/#faq_144';
     }
     $searchEngineUrls = Piwik_Common::getSearchEngineUrls();
diff --git a/plugins/Transitions/Transitions.php b/plugins/Transitions/Transitions.php
index 4adaf3f9974e684527b93c2ea45708965a79a0fa..765854bff7c50201e675c11276c1b4ffcc73713f 100644
--- a/plugins/Transitions/Transitions.php
+++ b/plugins/Transitions/Transitions.php
@@ -118,7 +118,7 @@ class Piwik_Transitions extends Piwik_Plugin
 
             foreach ($subData as &$row) {
                 if ($referrerType == Piwik_Common::REFERER_TYPE_SEARCH_ENGINE && empty($row['referrer_data'])) {
-                    $row['referrer_data'] = Piwik_Referers::LABEL_KEYWORD_NOT_DEFINED;
+                    $row['referrer_data'] = Piwik_Referers_API::LABEL_KEYWORD_NOT_DEFINED;
                 }
 
                 $referrerData[$referrerType][Piwik_Archive::INDEX_NB_VISITS] += $row[Piwik_Archive::INDEX_NB_VISITS];
diff --git a/plugins/UserCountry/API.php b/plugins/UserCountry/API.php
index d8cdb266b6736c54e78aa5c19515de9d52b9f4e1..36dfffe4186edf50284887090110f22517147d8b 100644
--- a/plugins/UserCountry/API.php
+++ b/plugins/UserCountry/API.php
@@ -32,7 +32,7 @@ class Piwik_UserCountry_API
 
     public function getCountry($idSite, $period, $date, $segment = false)
     {
-        $recordName = Piwik_UserCountry::VISITS_BY_COUNTRY_RECORD_NAME;
+        $recordName = Piwik_UserCountry_Archiving::VISITS_BY_COUNTRY_RECORD_NAME;
         $dataTable = $this->getDataTable($recordName, $idSite, $period, $date, $segment);
 
         // apply filter on the whole datatable in order the inline search to work (searches
@@ -48,7 +48,7 @@ class Piwik_UserCountry_API
 
     public function getContinent($idSite, $period, $date, $segment = false)
     {
-        $recordName = Piwik_UserCountry::VISITS_BY_COUNTRY_RECORD_NAME;
+        $recordName = Piwik_UserCountry_Archiving::VISITS_BY_COUNTRY_RECORD_NAME;
         $dataTable = $this->getDataTable($recordName, $idSite, $period, $date, $segment);
 
         $getContinent = array('Piwik_Common', 'getContinent');
@@ -71,10 +71,10 @@ class Piwik_UserCountry_API
      */
     public function getRegion($idSite, $period, $date, $segment = false)
     {
-        $recordName = Piwik_UserCountry::VISITS_BY_REGION_RECORD_NAME;
+        $recordName = Piwik_UserCountry_Archiving::VISITS_BY_REGION_RECORD_NAME;
         $dataTable = $this->getDataTable($recordName, $idSite, $period, $date, $segment);
 
-        $separator = Piwik_UserCountry::LOCATION_SEPARATOR;
+        $separator = Piwik_UserCountry_Archiving::LOCATION_SEPARATOR;
         $unk = Piwik_Tracker_Visit::UNKNOWN_CODE;
 
         // split the label and put the elements into the 'region' and 'country' metadata fields
@@ -114,10 +114,10 @@ class Piwik_UserCountry_API
      */
     public function getCity($idSite, $period, $date, $segment = false)
     {
-        $recordName = Piwik_UserCountry::VISITS_BY_CITY_RECORD_NAME;
+        $recordName = Piwik_UserCountry_Archiving::VISITS_BY_CITY_RECORD_NAME;
         $dataTable = $this->getDataTable($recordName, $idSite, $period, $date, $segment);
 
-        $separator = Piwik_UserCountry::LOCATION_SEPARATOR;
+         $separator = Piwik_UserCountry_Archiving::LOCATION_SEPARATOR;
         $unk = Piwik_Tracker_Visit::UNKNOWN_CODE;
 
         // split the label and put the elements into the 'city_name', 'region', 'country',
diff --git a/plugins/UserCountry/Archiving.php b/plugins/UserCountry/Archiving.php
new file mode 100644
index 0000000000000000000000000000000000000000..4c049ff6b9664a7fca381d6fc6a3afe7cbc4026d
--- /dev/null
+++ b/plugins/UserCountry/Archiving.php
@@ -0,0 +1,210 @@
+<?php
+/**
+ * Piwik - Open source web analytics
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ * @category Piwik_Plugins
+ * @package Piwik_UserCountry
+ */
+
+class Piwik_UserCountry_Archiving
+{
+    const VISITS_BY_COUNTRY_RECORD_NAME = 'UserCountry_country';
+    const VISITS_BY_REGION_RECORD_NAME = 'UserCountry_region';
+    const VISITS_BY_CITY_RECORD_NAME = 'UserCountry_city';
+    const DISTINCT_COUNTRIES_METRIC = 'UserCountry_distinctCountries';
+
+    // separate region, city & country info in stored report labels
+    const LOCATION_SEPARATOR = '|';
+
+    private $latLongForCities = array();
+
+    public function archiveDay($archiveProcessing)
+    {
+        $this->metricsByDimension = array('location_country' => array(),
+                                          'location_region'  => array(),
+                                          'location_city'    => array());
+        $this->aggregateFromVisits($archiveProcessing);
+        $this->aggregateFromConversions($archiveProcessing);
+        $this->archiveDayRecordInDatabase($archiveProcessing);
+    }
+
+    /**
+     * @param Piwik_ArchiveProcessing_Day $archiveProcessing
+     */
+    protected function aggregateFromVisits($archiveProcessing)
+    {
+        $dimensions = array_keys($this->metricsByDimension);
+        $query = $archiveProcessing->queryVisitsByDimension(
+            $dimensions,
+            $where = '',
+            $metrics = false,
+            $orderBy = false,
+            $rankingQuery = null,
+            $addSelect = 'MAX(log_visit.location_latitude) as location_latitude,
+						  MAX(log_visit.location_longitude) as location_longitude'
+        );
+
+        if ($query === false) {
+            return;
+        }
+
+        while ($row = $query->fetch()) {
+            $this->makeRegionCityLabelsUnique($row);
+            $this->rememberCityLatLong($row);
+            $this->aggregateConversion($archiveProcessing, $row);
+        }
+    }
+
+    /**
+     * Makes sure the region and city of a query row are unique.
+     *
+     * @param array $row
+     */
+    private function makeRegionCityLabelsUnique(&$row)
+    {
+        static $locationColumns = array('location_region', 'location_country', 'location_city');
+
+        // to be on the safe side, remove the location separator from the region/city/country we
+        // get from the query
+        foreach ($locationColumns as $column) {
+            $row[$column] = str_replace(self::LOCATION_SEPARATOR, '', $row[$column]);
+        }
+
+        if (!empty($row['location_region'])) // do not differentiate between unknown regions
+        {
+            $row['location_region'] = $row['location_region'] . self::LOCATION_SEPARATOR . $row['location_country'];
+        }
+
+        if (!empty($row['location_city'])) // do not differentiate between unknown cities
+        {
+            $row['location_city'] = $row['location_city'] . self::LOCATION_SEPARATOR . $row['location_region'];
+        }
+    }
+
+    protected function rememberCityLatLong($row)
+    {
+        $lat = $long = false;
+        if (!empty($row['location_city'])) {
+            if (!empty($row['location_latitude'])) {
+                $lat = $row['location_latitude'];
+            }
+            if (!empty($row['location_longitude'])) {
+                $long = $row['location_longitude'];
+            }
+        }
+
+        // store latitude/longitude, if we should
+        if ($lat !== false && $long !== false
+            && empty($this->latLongForCities[$row['location_city']])
+        ) {
+            $this->latLongForCities[$row['location_city']] = array($lat, $long);
+        }
+    }
+
+    protected function aggregateConversion($archiveProcessing, $row)
+    {
+        foreach ($this->metricsByDimension as $dimension => &$table) {
+            $label = (string)$row[$dimension];
+
+            if (!isset($table[$label])) {
+                $table[$label] = $archiveProcessing->makeEmptyRow();
+            }
+            $archiveProcessing->sumMetrics($row, $table[$label]);
+        }
+        return $table;
+    }
+
+    /**
+     * @param Piwik_ArchiveProcessing_Day $archiveProcessing
+     */
+    protected function aggregateFromConversions($archiveProcessing)
+    {
+        $dimensions = array_keys($this->metricsByDimension);
+        $query = $archiveProcessing->queryConversionsByDimension($dimensions);
+
+        if ($query === false) {
+            return;
+        }
+
+        while ($row = $query->fetch()) {
+            $this->makeRegionCityLabelsUnique($row);
+
+            $idGoal = $row['idgoal'];
+            foreach ($this->metricsByDimension as $dimension => &$table) {
+                $label = (string)$row[$dimension];
+
+                if (!isset($table[$label][Piwik_Archive::INDEX_GOALS][$idGoal])) {
+                    $table[$label][Piwik_Archive::INDEX_GOALS][$idGoal] = $archiveProcessing->makeEmptyGoalRow($idGoal);
+                }
+
+                $archiveProcessing->sumGoalMetrics($row, $table[$label][Piwik_Archive::INDEX_GOALS][$idGoal]);
+            }
+        }
+
+        foreach ($this->metricsByDimension as &$table) {
+            $archiveProcessing->enrichConversionsByLabelArray($table);
+        }
+    }
+
+    /**
+     * @param Piwik_ArchiveProcessing_Day $archiveProcessing
+     */
+    protected function archiveDayRecordInDatabase($archiveProcessing)
+    {
+        $maximumRows = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_standard'];
+
+        $tableCountry = Piwik_ArchiveProcessing_Day::getDataTableFromArray($this->metricsByDimension['location_country']);
+        $archiveProcessing->insertBlobRecord(self::VISITS_BY_COUNTRY_RECORD_NAME, $tableCountry->getSerialized());
+        $archiveProcessing->insertNumericRecord(self::DISTINCT_COUNTRIES_METRIC, $tableCountry->getRowsCount());
+        destroy($tableCountry);
+
+        $tableRegion = Piwik_ArchiveProcessing_Day::getDataTableFromArray($this->metricsByDimension['location_region']);
+        $serialized = $tableRegion->getSerialized($maximumRows, $maximumRows, Piwik_Archive::INDEX_NB_VISITS);
+        $archiveProcessing->insertBlobRecord(self::VISITS_BY_REGION_RECORD_NAME, $serialized);
+        destroy($tableRegion);
+
+        $tableCity = Piwik_ArchiveProcessing_Day::getDataTableFromArray($this->metricsByDimension['location_city']);
+        $this->setLatitudeLongitude($tableCity);
+        $serialized = $tableCity->getSerialized($maximumRows, $maximumRows, Piwik_Archive::INDEX_NB_VISITS);
+        $archiveProcessing->insertBlobRecord(self::VISITS_BY_CITY_RECORD_NAME, $serialized);
+        destroy($tableCity);
+    }
+
+    /**
+     * Utility method, appends latitude/longitude pairs to city table labels, if that data
+     * exists for the city.
+     */
+    private function setLatitudeLongitude($tableCity)
+    {
+        foreach ($tableCity->getRows() as $row) {
+            $label = $row->getColumn('label');
+            if (isset($this->latLongForCities[$label])) {
+                // get lat/long for city
+                list($lat, $long) = $this->latLongForCities[$label];
+                $lat = round($lat, Piwik_UserCountry_LocationProvider::GEOGRAPHIC_COORD_PRECISION);
+                $long = round($long, Piwik_UserCountry_LocationProvider::GEOGRAPHIC_COORD_PRECISION);
+
+                // set latitude + longitude metadata
+                $row->setMetadata('lat', $lat);
+                $row->setMetadata('long', $long);
+            }
+        }
+    }
+
+    public function archivePeriod($archiveProcessing)
+    {
+        $dataTableToSum = array(
+            self::VISITS_BY_COUNTRY_RECORD_NAME,
+            self::VISITS_BY_REGION_RECORD_NAME,
+            self::VISITS_BY_CITY_RECORD_NAME,
+        );
+
+        $nameToCount = $archiveProcessing->archiveDataTable($dataTableToSum);
+        $archiveProcessing->insertNumericRecord(self::DISTINCT_COUNTRIES_METRIC,
+            $nameToCount[self::VISITS_BY_COUNTRY_RECORD_NAME]['level0']);
+    }
+
+}
\ No newline at end of file
diff --git a/plugins/UserCountry/UserCountry.php b/plugins/UserCountry/UserCountry.php
index 03df8bc3f7ff27006c847ffce76d7692aff8013c..fb928ba67b237b24ec20e377ef83ef1cd987aaee 100644
--- a/plugins/UserCountry/UserCountry.php
+++ b/plugins/UserCountry/UserCountry.php
@@ -20,14 +20,6 @@ require_once PIWIK_INCLUDE_PATH . '/plugins/UserCountry/GeoIPAutoUpdater.php';
  */
 class Piwik_UserCountry extends Piwik_Plugin
 {
-    const VISITS_BY_COUNTRY_RECORD_NAME = 'UserCountry_country';
-    const VISITS_BY_REGION_RECORD_NAME = 'UserCountry_region';
-    const VISITS_BY_CITY_RECORD_NAME = 'UserCountry_city';
-
-    const DISTINCT_COUNTRIES_METRIC = 'UserCountry_distinctCountries';
-
-    // separate region, city & country info in stored report labels
-    const LOCATION_SEPARATOR = '|';
 
     public function getInformation()
     {
@@ -299,187 +291,30 @@ class Piwik_UserCountry extends Piwik_Plugin
      */
     function archivePeriod($notification)
     {
-        /**
-         * @param Piwik_ArchiveProcessing_Period $archiveProcessing
-         */
         $archiveProcessing = $notification->getNotificationObject();
+        if (!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) {
+            return;
+        }
 
-        if (!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return;
-
-        $dataTableToSum = array(
-            self::VISITS_BY_COUNTRY_RECORD_NAME,
-            self::VISITS_BY_REGION_RECORD_NAME,
-            self::VISITS_BY_CITY_RECORD_NAME,
-        );
-
-        $nameToCount = $archiveProcessing->archiveDataTable($dataTableToSum);
-        $archiveProcessing->insertNumericRecord(self::DISTINCT_COUNTRIES_METRIC,
-            $nameToCount[self::VISITS_BY_COUNTRY_RECORD_NAME]['level0']);
+        $archiving = new Piwik_UserCountry_Archiving();
+        $archiving->archivePeriod($archiveProcessing);
     }
 
-    private $interestTables = null;
-    private $latLongForCities = null;
-
     /**
      * @param Piwik_Event_Notification $notification  notification object
      * @return mixed
      */
     function archiveDay($notification)
     {
-        /**
-         * @var Piwik_ArchiveProcessing
-         */
         $archiveProcessing = $notification->getNotificationObject();
-
-        if (!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) return;
-
-        $this->interestTables = array('location_country' => array(),
-                                      'location_region'  => array(),
-                                      'location_city'    => array());
-        $this->latLongForCities = array();
-
-        $this->archiveDayAggregateVisits($archiveProcessing);
-        $this->archiveDayAggregateGoals($archiveProcessing);
-        $this->archiveDayRecordInDatabase($archiveProcessing);
-
-        unset($this->interestTables);
-        unset($this->latLongForCities);
-    }
-
-    /**
-     * @param Piwik_ArchiveProcessing_Day $archiveProcessing
-     */
-    protected function archiveDayAggregateVisits($archiveProcessing)
-    {
-        $dimensions = array_keys($this->interestTables);
-        $query = $archiveProcessing->queryVisitsByDimension(
-            $dimensions,
-            $where = '',
-            $metrics = false,
-            $orderBy = false,
-            $rankingQuery = null,
-            $addSelect = 'MAX(log_visit.location_latitude) as location_latitude,
-						  MAX(log_visit.location_longitude) as location_longitude'
-        );
-
-        if ($query === false) {
+        if (!$archiveProcessing->shouldProcessReportsForPlugin($this->getPluginName())) {
             return;
         }
 
-        while ($row = $query->fetch()) {
-            // get latitude/longitude if there's a city
-            $lat = $long = false;
-            if (!empty($row['location_city'])) {
-                if (!empty($row['location_latitude'])) {
-                    $lat = $row['location_latitude'];
-                }
-                if (!empty($row['location_longitude'])) {
-                    $long = $row['location_longitude'];
-                }
-            }
-
-            // make sure regions & cities w/ the same name don't get merged
-            $this->setLongCityRegionId($row);
-
-            // store latitude/longitude, if we should
-            if ($lat !== false && $long !== false) {
-                $this->latLongForCities[$row['location_city']] = array($lat, $long);
-            }
-
-            // add the stats to each dimension's table
-            foreach ($this->interestTables as $dimension => &$table) {
-                $label = (string)$row[$dimension];
-
-                if (!isset($table[$label])) {
-                    $table[$label] = $archiveProcessing->makeEmptyRow();
-                }
-                $archiveProcessing->sumMetrics($row, $table[$label]);
-            }
-        }
+        $archiving = new Piwik_UserCountry_Archiving();
+        $archiving->archiveDay($archiveProcessing);
     }
 
-    /**
-     * @param Piwik_ArchiveProcessing_Day $archiveProcessing
-     */
-    protected function archiveDayAggregateGoals($archiveProcessing)
-    {
-        $dimensions = array_keys($this->interestTables);
-        $query = $archiveProcessing->queryConversionsByDimension($dimensions);
-
-        if ($query === false) {
-            return;
-        }
-
-        while ($row = $query->fetch()) {
-            // make sure regions & cities w/ the same name don't get merged
-            $this->setLongCityRegionId($row);
-
-            $idGoal = $row['idgoal'];
-            foreach ($this->interestTables as $dimension => &$table) {
-                $label = (string)$row[$dimension];
-
-                if (!isset($table[$label][Piwik_Archive::INDEX_GOALS][$idGoal])) {
-                    $table[$label][Piwik_Archive::INDEX_GOALS][$idGoal] = $archiveProcessing->makeEmptyGoalRow($idGoal);
-                }
-
-                $archiveProcessing->sumGoalMetrics($row, $table[$label][Piwik_Archive::INDEX_GOALS][$idGoal]);
-            }
-        }
-
-        foreach ($this->interestTables as &$table) {
-            $archiveProcessing->enrichConversionsByLabelArray($table);
-        }
-    }
-
-    /**
-     * @param Piwik_ArchiveProcessing_Day $archiveProcessing
-     */
-    protected function archiveDayRecordInDatabase($archiveProcessing)
-    {
-        $maximumRows = Piwik_Config::getInstance()->General['datatable_archiving_maximum_rows_standard'];
-
-        $tableCountry = Piwik_ArchiveProcessing_Day::getDataTableFromArray($this->interestTables['location_country']);
-        $archiveProcessing->insertBlobRecord(self::VISITS_BY_COUNTRY_RECORD_NAME, $tableCountry->getSerialized());
-        $archiveProcessing->insertNumericRecord(self::DISTINCT_COUNTRIES_METRIC, $tableCountry->getRowsCount());
-        destroy($tableCountry);
-
-        $tableRegion = Piwik_ArchiveProcessing_Day::getDataTableFromArray($this->interestTables['location_region']);
-        $serialized = $tableRegion->getSerialized($maximumRows, $maximumRows, Piwik_Archive::INDEX_NB_VISITS);
-        $archiveProcessing->insertBlobRecord(self::VISITS_BY_REGION_RECORD_NAME, $serialized);
-        destroy($tableRegion);
-
-        $tableCity = Piwik_ArchiveProcessing_Day::getDataTableFromArray($this->interestTables['location_city']);
-        $this->setLatitudeLongitude($tableCity);
-        $serialized = $tableCity->getSerialized($maximumRows, $maximumRows, Piwik_Archive::INDEX_NB_VISITS);
-        $archiveProcessing->insertBlobRecord(self::VISITS_BY_CITY_RECORD_NAME, $serialized);
-        destroy($tableCity);
-    }
-
-    /**
-     * Makes sure the region and city of a query row are unique.
-     *
-     * @param array $row
-     */
-    private function setLongCityRegionId(&$row)
-    {
-        static $locationColumns = array('location_region', 'location_country', 'location_city');
-
-        // to be on the safe side, remove the location separator from the region/city/country we
-        // get from the query
-        foreach ($locationColumns as $column) {
-            $row[$column] = str_replace(self::LOCATION_SEPARATOR, '', $row[$column]);
-        }
-
-        if (!empty($row['location_region'])) // do not differentiate between unknown regions
-        {
-            $row['location_region'] = $row['location_region'] . self::LOCATION_SEPARATOR . $row['location_country'];
-        }
-
-        if (!empty($row['location_city'])) // do not differentiate between unknown cities
-        {
-            $row['location_city'] = $row['location_city'] . self::LOCATION_SEPARATOR . $row['location_region'];
-        }
-    }
 
     /**
      * Returns a list of country codes for a given continent code.
@@ -500,24 +335,4 @@ class Piwik_UserCountry extends Piwik_Plugin
                      'bind' => '-'); // HACK: SegmentExpression requires a $bind, even if there's nothing to bind
     }
 
-    /**
-     * Utility method, appends latitude/longitude pairs to city table labels, if that data
-     * exists for the city.
-     */
-    private function setLatitudeLongitude($tableCity)
-    {
-        foreach ($tableCity->getRows() as $row) {
-            $label = $row->getColumn('label');
-            if (isset($this->latLongForCities[$label])) {
-                // get lat/long for city
-                list($lat, $long) = $this->latLongForCities[$label];
-                $lat = round($lat, Piwik_UserCountry_LocationProvider::GEOGRAPHIC_COORD_PRECISION);
-                $long = round($long, Piwik_UserCountry_LocationProvider::GEOGRAPHIC_COORD_PRECISION);
-
-                // set latitude + longitude metadata
-                $row->setMetadata('lat', $lat);
-                $row->setMetadata('long', $long);
-            }
-        }
-    }
 }
diff --git a/plugins/UserCountry/functions.php b/plugins/UserCountry/functions.php
index ab1ff7e26a3ba7053997177de070049a5bc0a07b..072c54030bfee370555d7c3f52b2402bec65dee3 100644
--- a/plugins/UserCountry/functions.php
+++ b/plugins/UserCountry/functions.php
@@ -92,7 +92,7 @@ function Piwik_UserCountry_getRegionName($label)
         return Piwik_Translate('General_Unknown');
     }
 
-    list($regionCode, $countryCode) = explode(Piwik_UserCountry::LOCATION_SEPARATOR, $label);
+    list($regionCode, $countryCode) = explode(Piwik_UserCountry_Archiving::LOCATION_SEPARATOR, $label);
     return Piwik_UserCountry_LocationProvider_GeoIp::getRegionNameFromCodes($countryCode, $regionCode);
 }
 
@@ -114,7 +114,7 @@ function Piwik_UserCountry_getPrettyRegionName($label)
         return Piwik_Translate('General_Unknown');
     }
 
-    list($regionCode, $countryCode) = explode(Piwik_UserCountry::LOCATION_SEPARATOR, $label);
+    list($regionCode, $countryCode) = explode(Piwik_UserCountry_Archiving::LOCATION_SEPARATOR, $label);
 
     $result = Piwik_UserCountry_LocationProvider_GeoIp::getRegionNameFromCodes($countryCode, $regionCode);
     if ($countryCode != Piwik_Tracker_Visit::UNKNOWN_CODE && $countryCode != '') {
@@ -143,7 +143,7 @@ function Piwik_UserCountry_getPrettyCityName($label)
     }
 
     // get city name, region code & country code
-    $parts = explode(Piwik_UserCountry::LOCATION_SEPARATOR, $label);
+    $parts = explode(Piwik_UserCountry_Archiving::LOCATION_SEPARATOR, $label);
     $cityName = $parts[0];
     $regionCode = $parts[1];
     $countryCode = $parts[2];
diff --git a/tests/PHPUnit/Integration/expected/test_OneVisitorTwoVisits__Referers.getCleanKeyword.xml b/tests/PHPUnit/Integration/expected/test_OneVisitorTwoVisits__Referers.getCleanKeyword.xml
new file mode 100644
index 0000000000000000000000000000000000000000..07f3ff9a02d6436a03e1ec57b46ca47f3a010d89
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_OneVisitorTwoVisits__Referers.getCleanKeyword.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+	<error message="Please specify a value for 'label'.
+ 
+ --&gt; To temporarily debug this error further, set $SHOW_ME_BACKTRACE=true; in ResponseBuilder.php" />
+</result>
\ No newline at end of file
diff --git a/tests/PHPUnit/Integration/expected/test_OneVisitorTwoVisits__Referers.getKeywordNotDefinedString.xml b/tests/PHPUnit/Integration/expected/test_OneVisitorTwoVisits__Referers.getKeywordNotDefinedString.xml
new file mode 100644
index 0000000000000000000000000000000000000000..60b9a623a5848f56c2ee6a6dac2ce95a1a3d16a2
--- /dev/null
+++ b/tests/PHPUnit/Integration/expected/test_OneVisitorTwoVisits__Referers.getKeywordNotDefinedString.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>Keyword not defined</result>
\ No newline at end of file