Repository: openaddresses/machine
Pull request #582 (2017-03-22 19:45:14)

Dark launch population page
First part of #581 includes:

- PostGIS as part of standard Machine installation
- New schema with GPWv4 and Natural Earth data
- Stub `/coverage` page with no inbound links




Patch:
diff --git a/.travis.yml b/.travis.yml
index aa1758c..23ac01f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,7 +7,10 @@ cache:
   apt: true
   pip: true
 addons:
-  postgresql: "9.4"
+  postgresql: "9.3"
+before_install:
+  # Remove existing PostGIS to make room for our own.
+  - sudo apt-get remove -y postgis*
 install:
   # Install Machine globally via Chef recipe, to pick up complete dependencies.
   - sudo chef/run.sh prereqs
diff --git a/chef/database/recipes/default.rb b/chef/database/recipes/default.rb
index 0d9eb25..0d9bfb0 100644
--- a/chef/database/recipes/default.rb
+++ b/chef/database/recipes/default.rb
@@ -1,6 +1,23 @@
 # CircleCI has 9.3, while installing package 'postgresql' gets conflicting 9.5.
 package 'postgresql-9.3'
 
+package 'libsfcgal1' do
+  version '1.2.2-1~trusty2'
+end
+
+package 'liblwgeom-2.2-5' do
+  version '2.2.2+dfsg-2~trusty0'
+end
+
+package 'postgresql-9.3-postgis-2.2' do
+  version '2.2.2+dfsg-2~trusty0'
+end
+
+package 'postgresql-9.3-postgis-scripts' do
+  version '2.2.2+dfsg-2~trusty0'
+  options '--force-yes'
+end
+
 local = data_bag_item('data', 'local')
 user = local['db_user']
 pass = local['db_pass']
@@ -25,6 +42,7 @@ bash "create database" do
     psql #{args} -c "CREATE USER dashboard";
     psql #{args} -c "CREATE USER #{user} WITH SUPERUSER PASSWORD '#{pass}'";
     psql #{args} -c "CREATE DATABASE #{name} WITH OWNER #{user}";
+    psql #{args} -c "CREATE EXTENSION postgis" -d #{name};
   CODE
   
   # Stop as soon as an error is encountered.
diff --git a/chef/prereqs/recipes/default.rb b/chef/prereqs/recipes/default.rb
index 7c99f71..94a02e1 100644
--- a/chef/prereqs/recipes/default.rb
+++ b/chef/prereqs/recipes/default.rb
@@ -1,4 +1,5 @@
 execute 'add-apt-repository -y ppa:openaddresses/gdal2'
+execute 'add-apt-repository -y ppa:openaddresses/postgis2'
 execute 'apt-get update -y'
 
 package 'python3-pip'
diff --git a/circle.yml b/circle.yml
index af539dd..3127b85 100644
--- a/circle.yml
+++ b/circle.yml
@@ -6,6 +6,9 @@ machine:
     DATABASE_URL: postgres://openaddr:openaddr@localhost/openaddr
 
 dependencies:
+  pre:
+    # Remove existing PostGIS and Postgres to make room for our own.
+    - sudo apt-get autoremove -y postgis* postgresql*
   override:
     # Install Machine globally via Chef recipe, to pick up complete dependencies.
     - sudo chef/run.sh prereqs
diff --git a/openaddr/ci/coverage/__init__.py b/openaddr/ci/coverage/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/openaddr/ci/coverage/calculate.py b/openaddr/ci/coverage/calculate.py
new file mode 100644
index 0000000..dae7527
--- /dev/null
+++ b/openaddr/ci/coverage/calculate.py
@@ -0,0 +1,199 @@
+import logging; _L = logging.getLogger('openaddr.ci.coverage.calculate')
+
+import sys, json, os, tempfile
+from argparse import ArgumentParser
+from urllib.parse import urljoin
+
+import requests
+import psycopg2
+from osgeo import ogr
+
+from .. import setup_logger
+
+is_point = lambda geom: bool(geom.GetGeometryType() in (ogr.wkbPoint, ogr.wkbMultiPoint))
+is_polygon = lambda geom: bool(geom.GetGeometryType() in (ogr.wkbPolygon, ogr.wkbMultiPolygon))
+
+def validate_geometry(geometry):
+    '''
+    '''
+    if is_point(geometry):
+        # Points are easy, we love points
+        return geometry
+
+    elif is_polygon(geometry):
+        # Polygons may be invalid
+        if geometry.IsValid():
+            return geometry
+        else:
+            # Buffer by a tiny amount to force validity
+            return geometry.Buffer(0.00000001, 2)
+
+    else:
+        # Don't know what to do with other geometry types, so ignore them
+        return None
+
+def guess_iso_a2(feature):
+    '''
+    '''
+    iso_a2 = None
+
+    if feature.GetField('ISO 3166'):
+        # Read directly from ISO 3166 field
+        iso_a2 = feature.GetField('ISO 3166')
+
+    elif feature.GetField('ISO 3166-2'):
+        # Read from first half of dash-delimited ISO 3166-2 field
+        iso_a2, _ = feature.GetField('ISO 3166-2').split('-', 2)
+
+    elif feature.GetField('US Census GEOID'):
+        # Assume US based on Census GEOID
+        iso_a2 = 'US'
+
+    elif feature.GetField('source paths'):
+        # Read from paths, like "sources/xx/place.json"
+        paths = feature.GetField('source paths')
+        _, iso_a2, _ = paths.upper().split(os.path.sep, 2)
+
+    return iso_a2
+
+def insert_coverage_feature(db, feature):
+    ''' Add a feature of coverage to temporary rendered_world table.
+    '''
+    geom = validate_geometry(feature.GetGeometryRef())
+    iso_a2 = guess_iso_a2(feature)
+
+    if not geom:
+        return
+    
+    if is_point(geom):
+        # Ask PostGIS to buffer points by 10km, as a reasonable city size
+        db.execute('SELECT ST_AsText(ST_Buffer(%s::geography, 10000))', (geom.ExportToWkt(), ))
+        (geom_wkt, ) = db.fetchone()
+    else:
+        geom_wkt = geom.ExportToWkt()
+    
+    db.execute('''INSERT INTO rendered_world (iso_a2, count, geom)
+                  VALUES(%s, %s, ST_Multi(ST_SetSRID(%s::geometry, 4326)))''',
+               (iso_a2, feature.GetField('address count'), geom_wkt))
+    
+    return iso_a2
+
+def summarize_country_coverage(db, iso_a2):
+    ''' Populate area and population columns in areas table from gpwv4_2015 table.
+    '''
+    db.execute('''
+        WITH
+            --
+            -- 1x1 boxes of Natural Earth coverage, with GPWv4 population and area.
+            --
+            ne_boxes AS (
+            SELECT box.id, ne.name, gpw.area, gpw.population,
+                ST_Intersection(ne.geom, box.geom) AS geom
+            FROM ne_50m_admin_0_countries as ne, gpwv4_2015 as gpw, boxes as box
+            WHERE ne.iso_a2 = %s
+              AND ne.iso_a2 = gpw.iso_a2
+              AND gpw.box_id = box.id
+              AND box.size = 1.0
+            ),
+            --
+            -- 1x1 boxes of OpenAddresses coverage, shapes only.
+            --
+            oa_boxes AS (
+            SELECT box.id, ST_Intersection(oa.geom, box.geom) AS geom
+            FROM areas as oa, gpwv4_2015 as gpw, boxes as box
+            WHERE oa.iso_a2 = %s
+              AND oa.iso_a2 = gpw.iso_a2
+              AND gpw.box_id = box.id
+              AND box.size = 1.0
+            )
+
+        SELECT
+            --
+            -- Compare OA area coverage with NE area coverage for
+            -- each 1x1 degree box to estimate area and population.
+            --
+            SUM(ne_boxes.area * ST_Area(ST_Intersection(oa_boxes.geom, ne_boxes.geom)) / ST_Area(ne_boxes.geom)) AS area_total,
+            SUM(ne_boxes.population * ST_Area(ST_Intersection(oa_boxes.geom, ne_boxes.geom)) / ST_Area(ne_boxes.geom)) AS population_total,
+            SUM(ne_boxes.area * ST_Area(ST_Intersection(oa_boxes.geom, ne_boxes.geom)) / ST_Area(ne_boxes.geom)) / SUM(ne_boxes.area) AS area_pct,
+            SUM(ne_boxes.population * ST_Area(ST_Intersection(oa_boxes.geom, ne_boxes.geom)) / ST_Area(ne_boxes.geom)) / SUM(ne_boxes.population) AS population_pct,
+            MIN(ne_boxes.name) AS name
+
+        FROM ne_boxes LEFT JOIN oa_boxes
+        ON ne_boxes.id = oa_boxes.id
+        WHERE ST_Area(ne_boxes.geom) > 0;
+        ''',
+        (iso_a2, iso_a2))
+    
+    (area_total, pop_total, area_pct, pop_pct, name) = db.fetchone()
+    
+    db.execute('''UPDATE areas SET name = %s, area_total = %s, area_pct = %s,
+                  pop_total = %s, pop_pct = %s WHERE iso_a2 = %s''',
+               (name, area_total, area_pct, pop_total, pop_pct, iso_a2))
+
+START_URL = 'https://results.openaddresses.io/index.json'
+
+parser = ArgumentParser(description='Calculate current worldwide address coverage.')
+
+parser.add_argument('--sns-arn', default=os.environ.get('AWS_SNS_ARN', None),
+                    help='Optional AWS Simple Notification Service (SNS) resource. Defaults to value of AWS_SNS_ARN environment variable.')
+
+parser.add_argument('-v', '--verbose', help='Turn on verbose logging',
+                    action='store_const', dest='loglevel',
+                    const=logging.DEBUG, default=logging.INFO)
+
+parser.add_argument('-q', '--quiet', help='Turn off most logging',
+                    action='store_const', dest='loglevel',
+                    const=logging.WARNING, default=logging.INFO)
+
+def main():
+    '''
+    '''
+    args = parser.parse_args()
+    setup_logger(None, None, args.sns_arn, log_level=args.loglevel)
+    calculate(os.environ['DATABASE_URL'])
+
+def calculate(DATABASE_URL):
+    '''
+    '''
+    index = requests.get(START_URL).json()
+    geojson_url = urljoin(START_URL, index['render_geojson_url'])
+    _L.info('Downloading {}...'.format(geojson_url))
+
+    handle, filename = tempfile.mkstemp(prefix='render_geojson-', suffix='.geojson')
+    geojson = os.write(handle, requests.get(geojson_url).content)
+    os.close(handle)
+
+    with psycopg2.connect(DATABASE_URL) as conn:
+        with conn.cursor() as db:
+
+            ogr.UseExceptions()
+            iso_a2s = set()
+            rendered_ds = ogr.Open(filename)
+
+            db.execute('''
+                CREATE TEMPORARY TABLE rendered_world
+                (
+                    iso_a2  VARCHAR(2),
+                    count   INTEGER,
+                    geom    GEOMETRY(MultiPolygon, 4326)
+                );
+                ''')
+
+            for feature in rendered_ds.GetLayer(0):
+                iso_a2 = insert_coverage_feature(db, feature)
+                iso_a2s.add(iso_a2)
+                print(iso_a2, feature.GetField('address count'), 'addresses from', feature.GetField('source paths'), file=sys.stderr)
+        
+            db.execute('''
+                DELETE FROM areas;
+            
+                INSERT INTO areas (iso_a2, addr_count, buffer_km, geom)
+                SELECT iso_a2, SUM(count), 10, ST_Multi(ST_Union(geom))
+                FROM rendered_world GROUP BY iso_a2;
+                ''')
+        
+            for (index, iso_a2) in enumerate(sorted(iso_a2s)):
+                print('Counting up {} ({}/{})...'.format(iso_a2, index+1, len(iso_a2s)), file=sys.stderr)
+                summarize_country_coverage(db, iso_a2)
+
+    os.remove(filename)
diff --git a/openaddr/ci/coverage/schema.pgsql b/openaddr/ci/coverage/schema.pgsql
new file mode 100644
index 0000000..16dfbfa
--- /dev/null
+++ b/openaddr/ci/coverage/schema.pgsql
@@ -0,0 +1,86 @@
+CREATE EXTENSION IF NOT EXISTS postgis;
+
+DROP TABLE IF EXISTS gpwv4_2015;
+DROP TABLE IF EXISTS ne_50m_admin_0_countries;
+DROP TABLE IF EXISTS boxes;
+DROP TABLE IF EXISTS areas;
+
+CREATE TABLE boxes
+(
+    id          SERIAL PRIMARY KEY,
+    lon         FLOAT NOT NULL,
+    lat         FLOAT NOT NULL,
+    size        FLOAT NOT NULL,
+    geom        GEOMETRY(Polygon, 4326)
+);
+
+CREATE TABLE areas
+(
+    iso_a2      VARCHAR(2) PRIMARY KEY,
+    addr_count  INTEGER,
+    buffer_km   FLOAT,
+    geom        GEOMETRY(MultiPolygon, 4326),
+
+    name        TEXT,
+    area_total  INTEGER,
+    area_pct    FLOAT,
+    pop_total   INTEGER,
+    pop_pct     FLOAT,
+    
+    cpp_min     FLOAT,
+    cpp_avg     FLOAT,
+    cpp_max     FLOAT,
+    cpp_med     FLOAT,
+    cpp_stddev  FLOAT
+);
+
+CREATE TABLE gpwv4_2015
+(
+    iso_a2      VARCHAR(2),
+    iso_a3      VARCHAR(3),
+    box_id      INTEGER REFERENCES boxes(id),
+    population  FLOAT,
+    area        FLOAT
+);
+
+CREATE TABLE ne_50m_admin_0_countries
+(
+    gid         SERIAL PRIMARY KEY,
+    name        VARCHAR(80),
+    name_long   VARCHAR(80),
+    iso_a2      VARCHAR(80),
+    iso_a3      VARCHAR(80),
+    geom        GEOMETRY(MultiPolygon, 4326)
+);
+
+CREATE TEMPORARY TABLE gpwv4_2015_temp
+(
+    iso_a2      VARCHAR(2),
+    iso_a3      VARCHAR(3),
+    lon         FLOAT NOT NULL,
+    lat         FLOAT NOT NULL,
+    size        FLOAT NOT NULL,
+    population  FLOAT,
+    area        FLOAT
+);
+
+-- COPY gpwv4_2015_temp FROM '/tmp/gpwv4-2015-cut.csv' DELIMITER ',' HEADER CSV;
+-- 
+-- --
+-- -- Populate unique boxes from GPWv4 table
+-- --
+-- INSERT INTO boxes (lon, lat, size, geom)
+--     SELECT DISTINCT lon, lat, size,
+--     ST_SetSRID(ST_MakeBox2D(ST_MakePoint(lon, lat), ST_MakePoint(lon+size, lat+size)), 4326)
+--     FROM gpwv4_2015_temp;
+-- 
+-- ALTER TABLE boxes ADD UNIQUE (size, lon, lat);
+-- CREATE INDEX box_geoms ON boxes USING GIST (geom);
+-- 
+-- --
+-- -- Populate GPWv4 areas
+-- --
+-- INSERT INTO gpwv4_2015 (iso_a2, iso_a3, population, area, box_id)
+--     SELECT g.iso_a2, g.iso_a3, g.population, g.area, b.id
+--     FROM gpwv4_2015_temp AS g
+--     LEFT JOIN boxes AS b ON b.lat = g.lat AND b.lon = g.lon AND b.size = g.size;
diff --git a/openaddr/ci/recreate_db.py b/openaddr/ci/recreate_db.py
index 52003fd..250e12f 100644
--- a/openaddr/ci/recreate_db.py
+++ b/openaddr/ci/recreate_db.py
@@ -7,11 +7,17 @@ from psycopg2 import connect
 def recreate(DATABASE_URL):
     '''
     '''
-    schema_filename = join(dirname(__file__), 'schema.pgsql')
+    ci_schema_filename = join(dirname(__file__), 'schema.pgsql')
+    cov_schema_filename = join(dirname(__file__), 'coverage', 'schema.pgsql')
 
     with connect(DATABASE_URL) as conn:
         with conn.cursor() as db:
-            with open(schema_filename) as file:
+            db.execute('SET client_min_messages TO WARNING')
+        
+            with open(ci_schema_filename) as file:
+                db.execute(file.read())
+            
+            with open(cov_schema_filename) as file:
                 db.execute(file.read())
             
             db.execute('DROP TABLE IF EXISTS queue')
diff --git a/openaddr/ci/templates/base.html b/openaddr/ci/templates/base.html
index 7d27474..7b50c38 100644
--- a/openaddr/ci/templates/base.html
+++ b/openaddr/ci/templates/base.html
@@ -106,6 +106,7 @@
         <a href="{{ url_for('webhooks.app_index') }}">Download Data</a>
         <a href="{{ url_for('webhooks.app_get_sets') }}">All Batch Sets</a>
         <a href="{{ url_for('webhooks.app_get_jobs') }}">All CI Jobs</a>
+        <!--<a href="{{ url_for('webcoverage.get_coverage') }}">Coverage</a>-->
         <a href="{{ url_for('webhooks.app_get_dashboard') }}">Dashboard</a>
     </nav>
     <main>
diff --git a/openaddr/ci/templates/coverage.html b/openaddr/ci/templates/coverage.html
new file mode 100644
index 0000000..f4e54e5
--- /dev/null
+++ b/openaddr/ci/templates/coverage.html
@@ -0,0 +1,5 @@
+{% extends "base.html" %}
+{% block title %}Coverage{% endblock %}
+{% block main %}
+Yo.
+{% endblock main %}
diff --git a/openaddr/ci/web.py b/openaddr/ci/web.py
index ee51551..a39f50e 100644
--- a/openaddr/ci/web.py
+++ b/openaddr/ci/web.py
@@ -4,6 +4,7 @@ from werkzeug.contrib.fixers import ProxyFix
 from .webauth import apply_webauth_blueprint
 from .webhooks import apply_webhooks_blueprint
 from .webapi import apply_webapi_blueprint
+from .webcoverage import apply_coverage_blueprint
 from . import load_config
 
 app = Flask(__name__)
@@ -11,6 +12,7 @@ app.config.update(load_config())
 apply_webauth_blueprint(app)
 apply_webhooks_blueprint(app)
 apply_webapi_blueprint(app)
+apply_coverage_blueprint(app)
 
 # Look at X-Forwarded-* request headers when behind a proxy.
 app.wsgi_app = ProxyFix(app.wsgi_app)
diff --git a/openaddr/ci/webcoverage.py b/openaddr/ci/webcoverage.py
new file mode 100644
index 0000000..8c32d2d
--- /dev/null
+++ b/openaddr/ci/webcoverage.py
@@ -0,0 +1,24 @@
+import os
+
+from flask import Blueprint, render_template
+
+from . import setup_logger, webcommon
+
+webcoverage = Blueprint('webcoverage', __name__)
+
+@webcoverage.route('/coverage')
+@webcommon.log_application_errors
+def get_coverage():
+    return render_template('coverage.html')
+
+def apply_coverage_blueprint(app):
+    '''
+    '''
+    app.register_blueprint(webcoverage)
+
+    @app.before_first_request
+    def app_prepare():
+        setup_logger(None,
+                     None,
+                     os.environ.get('AWS_SNS_ARN'),
+                     webcommon.flask_log_level(app.config))
diff --git a/setup.py b/setup.py
index aac4c26..5bf0311 100644
--- a/setup.py
+++ b/setup.py
@@ -11,7 +11,7 @@ setup(
     author = 'Michal Migurski',
     author_email = 'mike-pypi@teczno.com',
     description = 'In-progress scripts for running OpenAddresses on a complete data set and publishing the results.',
-    packages = ['openaddr', 'openaddr.util', 'openaddr.ci', 'openaddr.tests', 'openaddr.parcels'],
+    packages = ['openaddr', 'openaddr.util', 'openaddr.ci', 'openaddr.ci.coverage', 'openaddr.tests', 'openaddr.parcels'],
     entry_points = dict(
         console_scripts = [
             'openaddr-render-us = openaddr.render:main',
@@ -26,6 +26,7 @@ setup(
             'openaddr-run-ec2-command = openaddr.run_ec2_ami:main',
             'openaddr-update-dotmap = openaddr.dotmap:main',
             'openaddr-sum-up-data = openaddr.ci.sum_up:main',
+            'openaddr-calculate-coverage = openaddr.ci.coverage.calculate:main',
         ]
     ),
     package_data = {
@@ -36,6 +37,9 @@ setup(
         'openaddr.ci': [
             'schema.pgsql', 'templates/*.*', 'static/*.*'
         ],
+        'openaddr.ci.coverage': [
+            'schema.pgsql'
+        ],
         'openaddr.tests': [
             'data/*.*', 'outputs/*.*', 'sources/*.*', 'sources/fr/*.*',
             'sources/us/*/*.*', 'sources/de/*.*', 'sources/nl/*.*',


Test patch:
diff --git a/openaddr/tests/ci.py b/openaddr/tests/ci.py
index 11abde1..8ca4bf9 100644
--- a/openaddr/tests/ci.py
+++ b/openaddr/tests/ci.py
@@ -29,7 +29,7 @@ from ..ci import (
     create_queued_job, TASK_QUEUE, DONE_QUEUE, DUE_QUEUE,
     enqueue_sources, find_batch_sources, render_set_maps, render_index_maps,
     is_merged_to_master, get_commit_info, HEARTBEAT_QUEUE, flush_heartbeat_queue,
-    get_recent_workers, load_config, get_batch_run_times, webauth,
+    get_recent_workers, load_config, get_batch_run_times, webauth, webcoverage,
     process_github_payload, skip_payload, is_rerun_payload, update_job_comments,
     reset_logger
     )
@@ -1207,6 +1207,7 @@ class TestAuth (unittest.TestCase):
             app.config.update(load_config(), MINIMUM_LOGLEVEL=logging.CRITICAL)
             apply_webhooks_blueprint(app)
             webauth.apply_webauth_blueprint(app)
+            webcoverage.apply_coverage_blueprint(app)
         
             client = app.test_client()
         
@@ -1268,6 +1269,7 @@ class TestHook (unittest.TestCase):
         self.app = Flask(__name__)
         self.app.config.update(load_config(), MINIMUM_LOGLEVEL=logging.CRITICAL)
         apply_webhooks_blueprint(self.app)
+        webcoverage.apply_coverage_blueprint(self.app)
 
         recreate_db.recreate(self.app.config['DATABASE_URL'])
 
diff --git a/openaddr/tests/coverage.py b/openaddr/tests/coverage.py
new file mode 100644
index 0000000..82bb970
--- /dev/null
+++ b/openaddr/tests/coverage.py
@@ -0,0 +1,50 @@
+import unittest
+import os
+
+import psycopg2
+from httmock import HTTMock, response
+
+DATABASE_URL = os.environ.get('DATABASE_URL', 'postgres:///hooked_on_sources')
+
+from ..ci import recreate_db
+from ..ci.coverage import calculate
+
+class TestCalculate (unittest.TestCase):
+
+    def setUp(self):
+        '''
+        '''
+        recreate_db.recreate(DATABASE_URL)
+        
+        with psycopg2.connect(DATABASE_URL) as conn:
+            with conn.cursor() as db:
+                db.execute("insert into ne_50m_admin_0_countries (gid, name, name_long, iso_a2, iso_a3, geom) values (1, 'Null Is.', 'Null Island', 'XX', 'XXX', '0106000020E610000002000000010300000001000000270000008EB1135E82533FBF691D554D1075EF3E90BE49D3A0683EBF664CC11A67D3F13EA208A9DBD9573EBF3FABCC94D6DFE23EEC3026FDBD143EBF8DEDB5A0F7C6E03E3659A31EA2D13DBF664CC11A67D3F13E13D21A834E083DBF54E41071732AE93EDCF3FC69A33A3DBF8DEDB5A0F7C6D03E0188BB7A15193DBF0000000000000000CCB6D3D688603CBF54E41071732AC93E0395F1EF332E3CBFF168E388B5F8E4BE7044F7AC6BB43CBF05A227655243EBBE7044F7AC6BB43CBF8DEDB5A0F7C6F0BE2829B000A60C3CBF664CC11A67D3F1BE16DF50F86C1D3CBF180AD80E46EC03BF7044F7AC6BB43CBF180AD80E46EC03BFA72215C616823CBF3677F4BF5C8B06BFEE3D5C72DC293DBF05A2276552430BBFA515DF50F86C3DBFC093162EABB009BF3659A31EA2D13DBF2D431CEBE2360ABF3659A31EA2D13DBF240F441669E20DBF344C6DA983BC3EBF240F441669E20DBF344C6DA983BC3EBFC093162EABB009BFD7D9907F66103FBFC093162EABB009BF691D554D10753FBF4BB0389CF9D50CBF61C1FD80070640BFE7340BB43BA408BFD5CC5A0A48FB3FBFA226FA7C941107BF213EB0E3BF4040BF664CC11A67D301BF4F779E78CE1640BFB75F3E59315CFDBE4F779E78CE1640BF05A227655243FBBEFCA9F1D24D6240BF54E41071732AF9BE213EB0E3BF4040BFF168E388B5F8E4BE46D26EF4311F40BFF168E388B5F8E4BE46D26EF4311F40BF54E41071732AD9BE2AE3DF675C3840BF00000000000000801FF5D72B2CB83FBF3FABCC94D6DFE23E56D3F544D7853FBF54E41071732AD93EA0FB7266BB423FBFB75F3E59315CDD3EA0FB7266BB423FBFA226FA7C9411E73E8EB1135E82533FBF691D554D1075EF3E010300000003000000C7010000C9C7EE022505163F7BA35698BED7303F2829B000A60C1C3F7689EAAD81AD323F1FF5D72B2CB81F3F41B8020AF5F4313F86C613419C87233F0ADAE4F04927323FA019C40776FC273FD2FBC6D79E59323F7D923B6C2233273F7689EAAD81AD323F9E0C8E9257E7283F613255302AA9333FE527D53E1D8F293FAB5AD2510E66333F99F221A81ABD2A3F3D9E961FB8CA333F72512D228AC92B3F2A5437177FDB333F261C7A8B87F72C3F3D9E961FB8CA333F240F441669E22D3FE02BBAF59A1E343FFA60191BBAD92F3FF5824F73F222333F8EB1135E82532F3F3FABCC94D6DF323FFA60191BBAD92F3FF78F85E81038323FFA60191BBAD92F3FE7525C55F65D313F0EF450DB8651303F1F317A6EA12B313FB345D26EF4312F3FD5CC5A0A48FB2F3F6B2A8BC22E8A2E3F46D26EF4311F303F92CB7F48BF7D2D3F4489963C9E962F3FBC79AA436E862B3F691D554D10752F3FE31A9FC9FE792A3F1FF5D72B2CB82F3FC093162EABB0293FD7D9907F66102F3FA019C40776FC273F33880FECF82F303F315D88D51F61283FE95F92CA1473303F6859F78F85E8203F46D26EF4311F303F664CC11A67D3213F581CCEFC6A0E303FF5824F73F222233F6B2A8BC22E8A2E3F3FABCC94D6DF223F48A30227DBC02D3FAB5AD2510E66233F92CB7F48BF7D2D3F3D9E961FB8CA233F95D8B5BDDD922C3FA94D9CDCEF50243F95D8B5BDDD922C3FF168E388B5F8243FDCF3FC69A33A2D3FEC4E779E78CE263FB96C74CE4F712C3FEA4141295AB9273F261C7A8B87F72C3F56F146E6913F283F261C7A8B87F72C3F0CC9C9C4AD82283FDE0033DFC14F2C3F97E5EB32FCA72B3FB96C74CE4F712C3FDE0033DFC14F2C3F4DBD6E1118EB2B3F240F441669E22D3FBE86E0B88C9B2A3F8EB1135E82532F3FBE86E0B88C9B2A3F213EB0E3BF40303F52D7DAFB54152A3F6859F78F85E8303F52D7DAFB54152A3F560F98874CF9303FC093162EABB0293F6859F78F85E8303F9E0C8E9257E7283F317BD976DA1A313F56F146E6913F283F1F317A6EA12B313F13F06B2409C2253F6859F78F85E8303F3A9160AA99B5243F8EB1135E82532F3F3A9160AA99B5243FB345D26EF4312F3F3FABCC94D6DF223F1FF5D72B2CB82F3FAE6708C72C7B223F46D26EF4311F303F1A170E846401233FE95F92CA1473303F3FABCC94D6DF223FC5CBD3B9A294303FF78F85E81038223F7BA35698BED7303FAE6708C72C7B223F1F317A6EA12B313FF78F85E81038223F6859F78F85E8303FFA9CBB5D2F4D213F560F98874CF9303FB28174B169A5203FE7525C55F65D313FFCA9F1D24D62203F8BE07F2BD9B1313F1FF5D72B2CB81F3F41B8020AF5F4313F213EB0E3BF40203FC0B167CF656A323FFCA9F1D24D62203F7689EAAD81AD323F691D554D10751F3FF5824F73F222333F691D554D10751F3FAB5AD2510E66333F90BE49D3A0681E3FAB5AD2510E66333F2829B000A60C1C3F4FE8F527F1B9333F2829B000A60C1C3F180AD80E46EC333F4BB0389CF9D51C3FBB97FBE42840343F4BB0389CF9D51C3F4DDBBFB2D2A4343F4FCAA48636001B3F3A9160AA99B5343F9E0C8E9257E7183FB98AC56F0A2B353FC5AD8218E8DA173F5D18E945ED7E353FE7340BB43BA4183F01A60C1CD0D2353FC5AD8218E8DA173FB77D8FFAEB15363F3677F4BF5C8B163F3677F4BF5C8B363F5D18E945ED7E153FEC4E779E78CE363F5D18E945ED7E153FC7BAB88D06F0363F3A9160AA99B5143FB5705985CD00373FAB5AD2510E66133F58FE7C5BB054373FF5824F73F222133F58FE7C5BB054373FCEE15AED612F143F21205F420587373FCEE15AED612F143FD7F7E12021CA373F3FABCC94D6DF123FD7F7E12021CA373FD2FBC6D79E59123F1F1329CDE671383FD2FBC6D79E59123FB056ED9A90D6383F664CC11A67D3113FB056ED9A90D6383F8DEDB5A0F7C6103F693BA6EECA2E383F05A2276552430B3F315D88D51F61383FC093162EABB0093F315D88D51F61383F3677F4BF5C8B063F7B8505F7031E383F180AD80E46EC033FD7F7E12021CA373F664CC11A67D3F13E6B48DC63E943373F180AD80E46ECF33EFE98D6A6B1BD363F8DEDB5A0F7C6E03EDC114E0B5EF4353F691D554D1075EF3E263ACB2C42B1353F54E41071732AE93E263ACB2C42B1353F54E41071732AD93E94F6065F984C353F54E41071732AD93E4ACE893DB48F353F8DEDB5A0F7C6E0BECCD42478431A353FA226FA7C9411E7BECCD42478431A353F180AD80E46ECF3BE15FDA19927D7343FF168E388B5F8F4BE15FDA19927D7343FB75F3E59315CFDBE84B9DDCB7D72343F90BE49D3A068FEBEF37519FED30D343F54E41071732AF9BE2A5437177FDB333F2D431CEBE236FABE05C078060DFD333FAB5AD2510E6603BF99107349D576333F180AD80E46EC03BF99107349D576333FA226FA7C941107BF7689EAAD81AD323FA226FA7C941107BF7689EAAD81AD323F54E41071732A09BFE54526E0D748323FC093162EABB009BF1C2444F98216323F4BB0389CF9D50CBFF78F85E81038323F691D554D10750FBFF78F85E81038323FB0743E3C4B9011BFC0B167CF656A323FFA9CBB5D2F4D11BF7689EAAD81AD323FD2FBC6D79E5912BFBEA4315A4755333F8DEDB5A0F7C610BF86C613419C87333F89D349B6BA9C12BF180AD80E46EC333FD2FBC6D79E5912BFF37519FED30D343FAB5AD2510E6613BF180AD80E46EC333F3A9160AA99B514BFCEE15AED612F343F3A9160AA99B514BFA94D9CDCEF50343F315D88D51F6118BF05C078060DFD333FE7340BB43BA418BF05C078060DFD333F766B990CC7F319BF86C613419C87333FBC79AA436E861BBF86C613419C87333F4BB0389CF9D51CBF613255302AA9333F4BB0389CF9D51CBF180AD80E46EC333F72512D228AC91BBFA94D9CDCEF50343F95D8B5BDDD921CBFF37519FED30D343F90BE49D3A0681EBFA94D9CDCEF50343FFCA9F1D24D6220BF97033DD4B661343F6859F78F85E820BFBB97FBE42840343FD508FD4CBD6E21BF4DDBBFB2D2A4343FD2FBC6D79E5922BFF168E388B5F8343FD2FBC6D79E5922BFCCD42478431A353F3FABCC94D6DF22BFF168E388B5F8343F86C613419C8723BF94F6065F984C353F5F251FBB0B9424BFC9C7EE022505363F5F251FBB0B9424BFB77D8FFAEB15363FF168E388B5F824BF01A60C1CD0D2353F38842A357BA025BF01A60C1CD0D2353FA43330F2B22626BFC9C7EE022505363F11E335AFEAAC26BFDC114E0B5EF4353FA226FA7C941127BF13F06B2409C2353F58FE7C5BB05427BF13F06B2409C2353FC5AD8218E8DA27BF4ACE893DB48F353FA019C40776FC27BF5D18E945ED7E353F0CC9C9C4AD8228BFEE5BAD1397E3353F2F505260014C29BFEE5BAD1397E3353F745E6397A8DE2ABF263ACB2C42B1353F4FCAA48636002BBF38842A357BA0353F7044F7AC6BB42CBF03B34291EEE7343F4BB0389CF9D52CBFF168E388B5F8343FDAE6C6F484252EBF284701A260C6343FB55208E412472EBF3A9160AA99B5343FD7D9907F66102FBF84B9DDCB7D72343FD7D9907F66102FBF97033DD4B661343F0EF450DB865130BFCEE15AED612F343FA03715A930B630BF3D9E961FB8CA333FC5CBD3B9A29430BF99107349D576333F560F98874CF930BF643F8BA548BE323F43C5387F130A31BFAE6708C72C7B323FC5CBD3B9A29430BF41B8020AF5F4313FC5CBD3B9A29430BF8BE07F2BD9B1313F46D26EF4311F30BF317BD976DA1A313FA03715A930B630BF6859F78F85E8303FB28174B169A530BF8DEDB5A0F7C6303F213EB0E3BF4030BFC5CBD3B9A294303F213EB0E3BF4030BFC5CBD3B9A294303FB28174B169A530BF0EF450DB8651303F317BD976DA1A31BFFCA9F1D24D62303FB0743E3C4B9031BF581CCEFC6A0E303F664CC11A67D331BFB345D26EF4312F3F41B8020AF5F431BFD7D9907F66102F3FD2FBC6D79E5932BF6B2A8BC22E8A2E3FD2FBC6D79E5932BF240F441669E22D3F1C2444F9821632BF4BB0389CF9D52C3FE54526E0D74832BF7044F7AC6BB42C3F78962023A0C231BF0395F1EF332E2C3F9D2ADF3312A131BF0395F1EF332E2C3F41B8020AF5F431BFE31A9FC9FE792A3F41B8020AF5F431BF766B990CC7F3293FE54526E0D74832BF54E41071732A293F1C2444F9821632BFE7340BB43BA4283FE54526E0D74832BF7B8505F7031E283F2F6EA301BC0532BFEA4141295AB9273F540262122EE431BFEA4141295AB9273FB0743E3C4B9031BFEA4141295AB9273F43C5387F130A31BFEA4141295AB9273F8DEDB5A0F7C630BFEC4E779E78CE263F7BA35698BED730BF5D18E945ED7E253F33880FECF82F30BF5D18E945ED7E253F1FF5D72B2CB82FBF15FDA19927D7243FD5CC5A0A48FB2FBF84B9DDCB7D72243FD5CC5A0A48FB2FBFA94D9CDCEF50243FB345D26EF4312FBF180AD80E46EC233FD7D9907F66102FBF3D9E961FB8CA233FB55208E412472EBF3A9160AA99B5243F240F441669E22DBF3A9160AA99B5243F4BB0389CF9D52CBFA7406667D13B253F4DBD6E1118EB2BBFCCD42478431A253FE10D6954E0642BBFCCD42478431A253F745E6397A8DE2ABF180AD80E46EC233FE10D6954E0642BBF3D9E961FB8CA233F99F221A81ABD2ABF1A170E846401233F2A36E675C4212BBF643F8BA548BE223F4FCAA48636002BBFD2FBC6D79E59223FE527D53E1D8F29BF664CC11A67D3213FE527D53E1D8F29BF1F317A6EA12B213FE7340BB43BA428BF1C2444F98216223F336ABE4A3E7627BFF78F85E81038223F809F71E1404826BF89D349B6BA9C223FC9C7EE02250526BF89D349B6BA9C223F38842A357BA025BF8BE07F2BD9B1213FC9C7EE02250526BFD508FD4CBD6E213FEE5BAD1397E325BF8DEDB5A0F7C6203F5B0BB3D0CE6926BF1FF5D72B2CB81F3F82ACA7565F5D25BF240F441669E21D3F38842A357BA025BF240F441669E21D3F15FDA19927D724BF72512D228AC91B3FF168E388B5F824BF72512D228AC91B3FA94D9CDCEF5024BF05A2276552431B3F3D9E961FB8CA23BFBC79AA436E861B3FD0EE9062804423BFDE0033DFC14F1C3F3FABCC94D6DF22BFDE0033DFC14F1C3FF78F85E8103822BFE31A9FC9FE791A3F89D349B6BA9C22BF2D431CEBE2361A3F5F251FBB0B9424BFE31A9FC9FE791A3FA7406667D13B25BFC093162EABB0193F38842A357BA025BF315D88D51F61183F82ACA7565F5D25BFC5AD8218E8DA173F3A9160AA99B524BF7B8505F7031E083F3A9160AA99B524BF7B8505F7031E083F82ACA7565F5D25BF05A2276552430B3F38842A357BA025BFFA9CBB5D2F4D113FA43330F2B22626BF84B9DDCB7D72143FA226FA7C941127BF58FE7C5BB054173F58FE7C5BB05427BF54E41071732A193F0FD6FF39CC9727BF0ABC934F8F6D193F7B8505F7031E28BF0FD6FF39CC97173F7978CF81E50829BF13F06B2409C2153F2F505260014C29BF3FABCC94D6DF123F99F221A81ABD2ABF43C5387F130A113FE10D6954E0642BBFC093162EABB0093FDE0033DFC14F2CBF213EB0E3BF40003FDCF3FC69A33A2DBF54E41071732AC93E240F441669E22DBFA226FA7C9411F7BEDAE6C6F484252EBF7B8505F7031E08BFDAE6C6F484252EBFCEE15AED612F14BFDAE6C6F484252EBFA7406667D13B15BFBE86E0B88C9B2ABFA7406667D13B15BF7B8505F7031E28BF13F06B2409C215BF315D88D51F6128BF0FD6FF39CC9717BF56F146E6913F28BFE7340BB43BA418BFE7340BB43BA428BFE7340BB43BA418BF2F505260014C29BF0FD6FF39CC9717BF9BFF571D39D229BFC5AD8218E8DA17BF2D431CEBE2362ABF9E0C8E9257E718BF08AF5DDA70582ABF0ABC934F8F6D19BFE10D6954E0642BBF2D431CEBE2361ABF0395F1EF332E2CBF2829B000A60C1CBF4DBD6E1118EB2BBF95D8B5BDDD921CBF95D8B5BDDD922CBF4696CCB1BCAB1EBF7044F7AC6BB42CBFD5CC5A0A48FB1FBF0395F1EF332E2CBFB28174B169A520BFDE0033DFC14F2CBFD508FD4CBD6E21BF7044F7AC6BB42CBF41B8020AF5F421BFB96C74CE4F712CBF1C2444F9821622BF4DBD6E1118EB2BBF3FABCC94D6DF22BF4DBD6E1118EB2BBF1A170E84640123BFE10D6954E0642BBF613255302AA923BF2A36E675C4212BBFCEE15AED612F24BFBC79AA436E862BBF5F251FBB0B9424BF4FCAA48636002BBF82ACA7565F5D25BF4FCAA48636002BBF13F06B2409C225BFBC79AA436E862BBF5B0BB3D0CE6926BFBC79AA436E862BBF3677F4BF5C8B26BF99F221A81ABD2ABF11E335AFEAAC26BF2D431CEBE2362ABF7D923B6C223327BFE527D53E1D8F29BFA019C40776FC27BFC093162EABB029BF0CC9C9C4AD8228BF54E41071732A29BF54E41071732A29BF54E41071732A29BFE527D53E1D8F29BFC3A04CA3C9C528BFE527D53E1D8F29BFA019C40776FC27BF766B990CC7F329BFC5AD8218E8DA27BF766B990CC7F329BFC7BAB88D06F026BFE527D53E1D8F29BFEE5BAD1397E325BF766B990CC7F329BF13F06B2409C225BF08AF5DDA70582ABFC7BAB88D06F026BF4FCAA48636002BBFA226FA7C941127BF05A2276552432BBF336ABE4A3E7627BF97E5EB32FCA72BBF0FD6FF39CC9727BF0395F1EF332E2CBF7D923B6C223327BF261C7A8B87F72CBFA226FA7C941127BF0188BB7A15192DBF809F71E1404826BF6D37C1374D9F2DBFC9C7EE02250526BF6D37C1374D9F2DBF84B9DDCB7D7224BFDAE6C6F484252EBF84B9DDCB7D7224BFDAE6C6F484252EBF613255302AA923BFB345D26EF4312FBFAE6708C72C7B22BFB345D26EF4312FBFB0743E3C4B9021BFFC6D4F90D8EE2EBF1F317A6EA12B21BFB345D26EF4312FBF6859F78F85E820BFB345D26EF4312FBF46D26EF4311F20BFFA60191BBAD92FBF46D26EF4311F20BF46D26EF4311F30BF43C5387F130A21BFFCA9F1D24D6230BF43C5387F130A21BFD71533C2DB8330BFB28174B169A520BF8DEDB5A0F7C630BFB28174B169A520BF6859F78F85E830BF46D26EF4311F20BF7BA35698BED730BFDAE6C6F484251EBFFA9CBB5D2F4D31BF95D8B5BDDD921CBF78962023A0C231BF240F441669E21DBF41B8020AF5F431BF0188BB7A15191DBF540262122EE431BFBC79AA436E861BBF0ADAE4F0492732BF4FCAA48636001BBF1C2444F9821632BF766B990CC7F319BF643F8BA548BE32BF2D431CEBE2361ABF1A170E84640133BF05A2276552431BBFBEA4315A475533BFC093162EABB019BFBEA4315A475533BF9E0C8E9257E718BF613255302AA933BFE7340BB43BA418BFBB97FBE4284034BF0FD6FF39CC9717BF726F7EC3448334BF54E41071732A19BFB98AC56F0A2B35BF315D88D51F6118BF6F62484E266E35BFA226FA7C941117BFC9C7EE02250536BF315D88D51F6118BF92E9D0E9793736BF7B8505F7031E18BF92E9D0E9793736BF13F06B2409C215BF48C153C8957A36BF5D18E945ED7E15BF48C153C8957A36BF180AD80E46EC13BF5B0BB3D0CE6936BFAB5AD2510E6613BF5B0BB3D0CE6936BF89D349B6BA9C12BFFE98D6A6B1BD36BF89D349B6BA9C12BF232D95B7239C36BF90BE49D3A0680EBFB5705985CD0037BF99F221A81ABD0ABFC7BAB88D06F036BF3677F4BF5C8B06BF7D923B6C223337BFF168E388B5F804BF21205F42058737BF3677F4BF5C8B06BFEA4141295AB937BFAB5AD2510E6603BF7B8505F7031E38BF180AD80E46EC03BF44A7E7DD585038BFD2FBC6D79E5902BF44A7E7DD585038BF05A227655243FBBE0CC9C9C4AD8238BF2D431CEBE236FABEB056ED9A90D638BF2D431CEBE236FABEC3A04CA3C9C538BF05A227655243EBBE419AB1683A3B39BFA226FA7C9411E7BE419AB1683A3B39BF00000000000000807978CF81E50839BF8DEDB5A0F7C6C03E7978CF81E50839BF54E41071732AE93EF8713447567E39BF54E41071732AE93ED3DD7536E49F39BF691D554D1075EF3EC093162EABB039BFC9C7EE022505F63E662E7079AC1939BFFA9CBB5D2F4D013F54E41071732A39BF180AD80E46EC033FFA7E6ABC749338BF05A2276552430B3F7B8505F7031E38BFDE0033DFC14F0C3F693BA6EECA2E38BF691D554D10750F3FC5AD8218E8DA37BF8DEDB5A0F7C6103F0FD6FF39CC9737BF8DEDB5A0F7C6103F90DC9A745B2237BF8DEDB5A0F7C6103F48C153C8957A36BF1C2444F98216123FA43330F2B22636BF1C2444F98216123F13F06B2409C235BFD2FBC6D79E59123F6F62484E266E35BFCEE15AED612F143F6F62484E266E35BFC9C7EE022505163F15FDA19927D734BF58FE7C5BB054173F97033DD4B66134BF0FD6FF39CC97173FF37519FED30D34BFE7340BB43BA4183F99107349D57633BFE7340BB43BA4183F86C613419C8733BF05A2276552431B3F2C616D8C9DF032BF4BB0389CF9D51C3F9B1DA9BEF38B32BF72512D228AC91B3F2F6EA301BC0532BF4FCAA48636001B3F1C2444F9821632BF90BE49D3A0681E3F7689EAAD81AD32BF4696CCB1BCAB1E3F7689EAAD81AD32BF213EB0E3BF40203FF78F85E8103832BF8DEDB5A0F7C6203FF78F85E8103832BF643F8BA548BE223F1C2444F9821632BF1A170E846401233F8BE07F2BD9B131BFF5824F73F222233FFA9CBB5D2F4D31BFF168E388B5F8243F1F317A6EA12B31BF38842A357BA0253FD71533C2DB8330BFA7406667D13B253F46D26EF4311F30BF11E335AFEAAC263F691D554D10752FBF3677F4BF5C8B263FFF7A8505F7032EBF0FD6FF39CC97273FDCF3FC69A33A2DBF0FD6FF39CC97273F4BB0389CF9D52CBF7978CF81E508293F0395F1EF332E2CBF2F505260014C293F97E5EB32FCA72BBFBE86E0B88C9B2A3F9BFF571D39D229BF0395F1EF332E2C3FE527D53E1D8F29BF0188BB7A15192D3FEA4141295AB927BFB96C74CE4F712C3FC5AD8218E8DA27BF72512D228AC92B3FE7340BB43BA428BFE31A9FC9FE792A3F315D88D51F6128BF9BFF571D39D2293F315D88D51F6128BF2F505260014C293FE7340BB43BA428BFA019C40776FC273FA019C40776FC27BFA019C40776FC273FA226FA7C941127BFEC4E779E78CE263F3677F4BF5C8B26BFEC4E779E78CE263F82ACA7565F5D25BF56F146E6913F283F15FDA19927D724BF315D88D51F61283FCEE15AED612F24BF9E0C8E9257E7283FAB5AD2510E6623BF7B8505F7031E283F1C2444F9821622BF7978CF81E508293FD71533C2DB8320BFC3A04CA3C9C5283FFCA9F1D24D6220BFC5AD8218E8DA273FFC6D4F90D8EE1EBFEA4141295AB9273F4696CCB1BCAB1EBF9E0C8E9257E7283FBC79AA436E861BBF7978CF81E508293FBC79AA436E861BBF97E5EB32FCA72B3FDE0033DFC14F1CBF95D8B5BDDD922C3F4FCAA48636001BBF4BB0389CF9D52C3F99F221A81ABD1ABFDAE6C6F484252E3FC5AD8218E8DA17BF21020EA14ACD2E3FEC4E779E78CE16BF6B2A8BC22E8A2E3FEC4E779E78CE16BF691D554D10752F3F13F06B2409C215BF581CCEFC6A0E303F613255302AA913BF33880FECF82F303F43C5387F130A11BFB345D26EF4312F3FFC6D4F90D8EE0EBFD7D9907F66102F3F90BE49D3A0680EBF4696CCB1BCAB2E3FF168E388B5F804BF6B2A8BC22E8A2E3F84B9DDCB7D7204BF4489963C9E962F3F691D554D1075FFBEFA60191BBAD92F3F691D554D1075FFBEFC6D4F90D8EE2E3F05A227655243FBBEFC6D4F90D8EE2E3F3FABCC94D6DFF2BE0188BB7A15192D3FF168E388B5F8E4BE0188BB7A15192D3FB75F3E59315CDDBEB96C74CE4F712C3F54E41071732AC9BEB96C74CE4F712C3F54E41071732AC93E97E5EB32FCA72B3FF168E388B5F8E43E0395F1EF332E2C3F54E41071732AE93E0188BB7A15192D3F664CC11A67D3F13E0188BB7A15192D3F691D554D1075FF3E6B2A8BC22E8A2E3F664CC11A67D3013F90BE49D3A0682E3F3FABCC94D6DF023F4489963C9E962F3F43C5387F130A113F8EB1135E82532F3F1C2444F98216123FFA60191BBAD92F3FD2FBC6D79E59123F213EB0E3BF40303F8DEDB5A0F7C6103F213EB0E3BF40303FDE0033DFC14F0C3FA03715A930B6303F240F441669E20D3F43C5387F130A313F240F441669E20D3F2F6EA301BC05323F691D554D10750F3F0ADAE4F04927323F8DEDB5A0F7C6103F0CE71A66683C313F1C2444F98216123F1F317A6EA12B313FF5824F73F222133F7BA35698BED7303FC9C7EE022505163F7BA35698BED7303F10000000F5824F73F22213BFC7BAB88D06F026BFFA9CBB5D2F4D11BF11E335AFEAAC26BF3677F4BF5C8B06BFEE5BAD1397E325BF664CC11A67D3F1BE5D18E945ED7E25BF8DEDB5A0F7C6F0BE5F251FBB0B9424BF240F441669E20DBF5F251FBB0B9424BF90BE49D3A0680EBF5D18E945ED7E25BF213EB0E3BF4010BF13F06B2409C225BFFA9CBB5D2F4D11BF5D18E945ED7E25BFB0743E3C4B9011BF180AD80E46EC23BF3FABCC94D6DF12BF180AD80E46EC23BF3FABCC94D6DF12BF5F251FBB0B9424BF664CC11A67D311BFF168E388B5F824BFD2FBC6D79E5912BF38842A357BA025BFD2FBC6D79E5912BF5B0BB3D0CE6926BFF5824F73F22213BFC7BAB88D06F026BF06000000B75F3E59315CDDBE5F251FBB0B9424BFB75F3E59315CDDBEA7406667D13B25BF691D554D1075EF3ECCD42478431A25BFAB5AD2510E66033FA7406667D13B25BFAB5AD2510E66033F3A9160AA99B524BFB75F3E59315CDDBE5F251FBB0B9424BF')")
+                db.execute("insert into boxes (id, lon, lat, size, geom) values (1,  0,  0, 1, st_setsrid('polygon(( 0  0,  0  1,  1  1,  1  0,  0  0))'::geometry, 4326))")
+                db.execute("insert into boxes (id, lon, lat, size, geom) values (2,  0, -1, 1, st_setsrid('polygon(( 0 -1,  0  0,  1  0,  1 -1,  0 -1))'::geometry, 4326))")
+                db.execute("insert into boxes (id, lon, lat, size, geom) values (3, -1, -1, 1, st_setsrid('polygon((-1 -1, -1  0,  0  0,  0 -1, -1 -1))'::geometry, 4326))")
+                db.execute("insert into boxes (id, lon, lat, size, geom) values (4, -1,  0, 1, st_setsrid('polygon((-1  0, -1  1,  0  1,  0  0, -1  0))'::geometry, 4326))")
+                db.execute("insert into gpwv4_2015 (iso_a2, box_id, population, area) values ('XX', 1, 2000, 800)")
+                db.execute("insert into gpwv4_2015 (iso_a2, box_id, population, area) values ('XX', 2, 4000, 600)")
+                db.execute("insert into gpwv4_2015 (iso_a2, box_id, population, area) values ('XX', 3, 6000, 400)")
+                db.execute("insert into gpwv4_2015 (iso_a2, box_id, population, area) values ('XX', 4, 8000, 200)")
+                    
+    def test_calculate(self):
+
+        def response_geojson(url, request):
+            if (request.method, url.hostname, url.path) == ('GET', 'results.openaddresses.io', '/index.json'):
+                return response(200, b'{"render_geojson_url": "http://data.openaddresses.io/render-world.geojson"}', headers={'Content-Type': 'application/json'})
+            
+            if (request.method, url.hostname, url.path) == ('GET', 'data.openaddresses.io', '/render-world.geojson'):
+                null_geojson = '''{\n"type": "FeatureCollection",\n"features": [\n{ "type": "Feature", "properties": {"source count": 1, "name": "Null Island", "source dates": "2017-03-12 21:54:49.107291+00:00", "source paths": "sources/xx/countrywide.json", "ISO 3166": "XX", "status": "good", "address count": 9999}, "geometry": { "type": "MultiPolygon", "coordinates": [ [ [ [ -0.000478, 0.000015 ], [ -0.000464, 0.000017 ], [ -0.000463, 0.000009 ], [ -0.000459, 0.000008 ], [ -0.000455, 0.000017 ], [ -0.000443, 0.000012 ], [ -0.000446, 0.000004 ], [ -0.000444, 0.0 ], [ -0.000433, 0.000003 ], [ -0.00043, -0.00001 ], [ -0.000438, -0.000013 ], [ -0.000438, -0.000016 ], [ -0.000428, -0.000017 ], [ -0.000429, -0.000038 ], [ -0.000438, -0.000038 ], [ -0.000435, -0.000043 ], [ -0.000445, -0.000052 ], [ -0.000449, -0.000049 ], [ -0.000455, -0.00005 ], [ -0.000455, -0.000057 ], [ -0.000469, -0.000057 ], [ -0.000469, -0.000049 ], [ -0.000474, -0.000049 ], [ -0.00048, -0.000055 ], [ -0.000489, -0.000047 ], [ -0.000488, -0.000044 ], [ -0.000496, -0.000034 ], [ -0.000491, -0.000028 ], [ -0.000491, -0.000026 ], [ -0.0005, -0.000024 ], [ -0.000496, -0.00001 ], [ -0.000492, -0.00001 ], [ -0.000492, -0.000006 ], [ -0.000495, -0.0 ], [ -0.000484, 0.000009 ], [ -0.000481, 0.000006 ], [ -0.000477, 0.000007 ], [ -0.000477, 0.000011 ], [ -0.000478, 0.000015 ] ] ], [ [ [ 0.000084, 0.000257 ], [ 0.000107, 0.000285 ], [ 0.000121, 0.000274 ], [ 0.000149, 0.000277 ], [ 0.000183, 0.00028 ], [ 0.000177, 0.000285 ], [ 0.00019, 0.0003 ], [ 0.000195, 0.000296 ], [ 0.000204, 0.000302 ], [ 0.000212, 0.000303 ], [ 0.000221, 0.000302 ], [ 0.000228, 0.000307 ], [ 0.000243, 0.000292 ], [ 0.000239, 0.000288 ], [ 0.000243, 0.000278 ], [ 0.000243, 0.000265 ], [ 0.000249, 0.000262 ], [ 0.000238, 0.000244 ], [ 0.000233, 0.000246 ], [ 0.000225, 0.000241 ], [ 0.00021, 0.00024 ], [ 0.000202, 0.000242 ], [ 0.000196, 0.000237 ], [ 0.000183, 0.000247 ], [ 0.000186, 0.000251 ], [ 0.000129, 0.000246 ], [ 0.000136, 0.000245 ], [ 0.000146, 0.000233 ], [ 0.000144, 0.000227 ], [ 0.000148, 0.000225 ], [ 0.000151, 0.000218 ], [ 0.000155, 0.000218 ], [ 0.00016, 0.000223 ], [ 0.000174, 0.000217 ], [ 0.000181, 0.000221 ], [ 0.000185, 0.000221 ], [ 0.000187, 0.000216 ], [ 0.000211, 0.000217 ], [ 0.000216, 0.000213 ], [ 0.000228, 0.000203 ], [ 0.000239, 0.000203 ], [ 0.000248, 0.000199 ], [ 0.000258, 0.000199 ], [ 0.000259, 0.000196 ], [ 0.000258, 0.00019 ], [ 0.000261, 0.000185 ], [ 0.000262, 0.000166 ], [ 0.000258, 0.000158 ], [ 0.000239, 0.000158 ], [ 0.000238, 0.000144 ], [ 0.000242, 0.000141 ], [ 0.000246, 0.000145 ], [ 0.000251, 0.000144 ], [ 0.000253, 0.000139 ], [ 0.000257, 0.000141 ], [ 0.000262, 0.000139 ], [ 0.000258, 0.000132 ], [ 0.000259, 0.000127 ], [ 0.000265, 0.000125 ], [ 0.00027, 0.000121 ], [ 0.000274, 0.000124 ], [ 0.000281, 0.000125 ], [ 0.000285, 0.00012 ], [ 0.000292, 0.00012 ], [ 0.000296, 0.000116 ], [ 0.000296, 0.000107 ], [ 0.000301, 0.000107 ], [ 0.000304, 0.00011 ], [ 0.000309, 0.00011 ], [ 0.000315, 0.000103 ], [ 0.000316, 0.000095 ], [ 0.000323, 0.000091 ], [ 0.000328, 0.000094 ], [ 0.000333, 0.000091 ], [ 0.000337, 0.000086 ], [ 0.000344, 0.000082 ], [ 0.000348, 0.000082 ], [ 0.00035, 0.000079 ], [ 0.000351, 0.000074 ], [ 0.000356, 0.000073 ], [ 0.000356, 0.000077 ], [ 0.000359, 0.000077 ], [ 0.000363, 0.000072 ], [ 0.000363, 0.00007 ], [ 0.000373, 0.00007 ], [ 0.000379, 0.000068 ], [ 0.000379, 0.000064 ], [ 0.000369, 0.000052 ], [ 0.000372, 0.000049 ], [ 0.000372, 0.000043 ], [ 0.000368, 0.000038 ], [ 0.000363, 0.000017 ], [ 0.000355, 0.000019 ], [ 0.000347, 0.000008 ], [ 0.000335, 0.000015 ], [ 0.000331, 0.000012 ], [ 0.000331, 0.000006 ], [ 0.000325, 0.000006 ], [ 0.000329, -0.000008 ], [ 0.000322, -0.000011 ], [ 0.000322, -0.000019 ], [ 0.000318, -0.00002 ], [ 0.000318, -0.000028 ], [ 0.000312, -0.000029 ], [ 0.000306, -0.000024 ], [ 0.000303, -0.000025 ], [ 0.000305, -0.000037 ], [ 0.000297, -0.000038 ], [ 0.000297, -0.000044 ], [ 0.000285, -0.000044 ], [ 0.000285, -0.000048 ], [ 0.000279, -0.000049 ], [ 0.000276, -0.000055 ], [ 0.000278, -0.00006 ], [ 0.000278, -0.000067 ], [ 0.000281, -0.000066 ], [ 0.000285, -0.00007 ], [ 0.000295, -0.000064 ], [ 0.000298, -0.000071 ], [ 0.000304, -0.00007 ], [ 0.000306, -0.000074 ], [ 0.000304, -0.000079 ], [ 0.000308, -0.000079 ], [ 0.00031, -0.000093 ], [ 0.000305, -0.000094 ], [ 0.000305, -0.000099 ], [ 0.000298, -0.000105 ], [ 0.000298, -0.00011 ], [ 0.0003, -0.00011 ], [ 0.000304, -0.000106 ], [ 0.00031, -0.000109 ], [ 0.000306, -0.000116 ], [ 0.00031, -0.000125 ], [ 0.000311, -0.000129 ], [ 0.000309, -0.000133 ], [ 0.000315, -0.00014 ], [ 0.00032, -0.00014 ], [ 0.000322, -0.000144 ], [ 0.00032, -0.000149 ], [ 0.000325, -0.000157 ], [ 0.000336, -0.000157 ], [ 0.000337, -0.00016 ], [ 0.000333, -0.000165 ], [ 0.000333, -0.000169 ], [ 0.000336, -0.000173 ], [ 0.000335, -0.000176 ], [ 0.000332, -0.000178 ], [ 0.000332, -0.000182 ], [ 0.000329, -0.000183 ], [ 0.000328, -0.000187 ], [ 0.000334, -0.000193 ], [ 0.000334, -0.000205 ], [ 0.000331, -0.000206 ], [ 0.00033, -0.000219 ], [ 0.000319, -0.00022 ], [ 0.00032, -0.00023 ], [ 0.000317, -0.000231 ], [ 0.000316, -0.000237 ], [ 0.000312, -0.000237 ], [ 0.000311, -0.000249 ], [ 0.000308, -0.000255 ], [ 0.000302, -0.000253 ], [ 0.000297, -0.000259 ], [ 0.000286, -0.00026 ], [ 0.000282, -0.000253 ], [ 0.000274, -0.000253 ], [ 0.00027, -0.000246 ], [ 0.000261, -0.000255 ], [ 0.000258, -0.000254 ], [ 0.000256, -0.000248 ], [ 0.000253, -0.000248 ], [ 0.000253, -0.000254 ], [ 0.000249, -0.000261 ], [ 0.00025, -0.000268 ], [ 0.000245, -0.000272 ], [ 0.000238, -0.000274 ], [ 0.000237, -0.00028 ], [ 0.000233, -0.00028 ], [ 0.000228, -0.000276 ], [ 0.00022, -0.000279 ], [ 0.000219, -0.000271 ], [ 0.000215, -0.000269 ], [ 0.000215, -0.000274 ], [ 0.000202, -0.000274 ], [ 0.000198, -0.000279 ], [ 0.000192, -0.000276 ], [ 0.000188, -0.000279 ], [ 0.000184, -0.000275 ], [ 0.000181, -0.000273 ], [ 0.000181, -0.000268 ], [ 0.000181, -0.00026 ], [ 0.000181, -0.000256 ], [ 0.000174, -0.000257 ], [ 0.000164, -0.000247 ], [ 0.000164, -0.000242 ], [ 0.000159, -0.000244 ], [ 0.000156, -0.000244 ], [ 0.000155, -0.000238 ], [ 0.000152, -0.000237 ], [ 0.000151, -0.000231 ], [ 0.000158, -0.000228 ], [ 0.000158, -0.00022 ], [ 0.000162, -0.000213 ], [ 0.000161, -0.000209 ], [ 0.000161, -0.000205 ], [ 0.000152, -0.000209 ], [ 0.000151, -0.000204 ], [ 0.000145, -0.000207 ], [ 0.000143, -0.000206 ], [ 0.00014, -0.000195 ], [ 0.000136, -0.000195 ], [ 0.000131, -0.000188 ], [ 0.000138, -0.000179 ], [ 0.000139, -0.00017 ], [ 0.000142, -0.000168 ], [ 0.000142, -0.000165 ], [ 0.000135, -0.000168 ], [ 0.000133, -0.000167 ], [ 0.000128, -0.000171 ], [ 0.000121, -0.000163 ], [ 0.000114, -0.000165 ], [ 0.000114, -0.000159 ], [ 0.000106, -0.00016 ], [ 0.000106, -0.000155 ], [ 0.000104, -0.000151 ], [ 0.000105, -0.000147 ], [ 0.000108, -0.000144 ], [ 0.000108, -0.000139 ], [ 0.000101, -0.000142 ], [ 0.0001, -0.000157 ], [ 0.000101, -0.000162 ], [ 0.000098, -0.000165 ], [ 0.000093, -0.000163 ], [ 0.000091, -0.000158 ], [ 0.000046, -0.000158 ], [ 0.000046, -0.000163 ], [ 0.000052, -0.000165 ], [ 0.000066, -0.000169 ], [ 0.000078, -0.000176 ], [ 0.000089, -0.000178 ], [ 0.000096, -0.00018 ], [ 0.000097, -0.000184 ], [ 0.00009, -0.000191 ], [ 0.000083, -0.000193 ], [ 0.000072, -0.000204 ], [ 0.000065, -0.000209 ], [ 0.000049, -0.000216 ], [ 0.000031, -0.000223 ], [ 0.000003, -0.000228 ], [ -0.000022, -0.00023 ], [ -0.000046, -0.00023 ], [ -0.000077, -0.00023 ], [ -0.000081, -0.000203 ], [ -0.000081, -0.000184 ], [ -0.000083, -0.000186 ], [ -0.00009, -0.000185 ], [ -0.000094, -0.000188 ], [ -0.000094, -0.000193 ], [ -0.00009, -0.000197 ], [ -0.000091, -0.0002 ], [ -0.000095, -0.000201 ], [ -0.000097, -0.000209 ], [ -0.0001, -0.000215 ], [ -0.000107, -0.000213 ], [ -0.000109, -0.000218 ], [ -0.000117, -0.000219 ], [ -0.000122, -0.000215 ], [ -0.000127, -0.000216 ], [ -0.000133, -0.000219 ], [ -0.000137, -0.000217 ], [ -0.000138, -0.000213 ], [ -0.000144, -0.000213 ], [ -0.000145, -0.000209 ], [ -0.00015, -0.000207 ], [ -0.000154, -0.00021 ], [ -0.000157, -0.000206 ], [ -0.000163, -0.000206 ], [ -0.000166, -0.00021 ], [ -0.000171, -0.00021 ], [ -0.000172, -0.000204 ], [ -0.000173, -0.0002 ], [ -0.000177, -0.000195 ], [ -0.000183, -0.000196 ], [ -0.000187, -0.000192 ], [ -0.000192, -0.000192 ], [ -0.000195, -0.000189 ], [ -0.000195, -0.000183 ], [ -0.000198, -0.000182 ], [ -0.000198, -0.000175 ], [ -0.000195, -0.000167 ], [ -0.000198, -0.000166 ], [ -0.000201, -0.000175 ], [ -0.000206, -0.000176 ], [ -0.000208, -0.000179 ], [ -0.000211, -0.00018 ], [ -0.000215, -0.000177 ], [ -0.000221, -0.000176 ], [ -0.000222, -0.00017 ], [ -0.000226, -0.000168 ], [ -0.000226, -0.000156 ], [ -0.00023, -0.000156 ], [ -0.00023, -0.00015 ], [ -0.000238, -0.000141 ], [ -0.000238, -0.000134 ], [ -0.000236, -0.000131 ], [ -0.000238, -0.000129 ], [ -0.000238, -0.000123 ], [ -0.000243, -0.000123 ], [ -0.000246, -0.00013 ], [ -0.00025, -0.00013 ], [ -0.000252, -0.000127 ], [ -0.000256, -0.000127 ], [ -0.000258, -0.000123 ], [ -0.000257, -0.000115 ], [ -0.000264, -0.000109 ], [ -0.000271, -0.000114 ], [ -0.000274, -0.000111 ], [ -0.000273, -0.000105 ], [ -0.000277, -0.000103 ], [ -0.000276, -0.000099 ], [ -0.000286, -0.0001 ], [ -0.00029, -0.000104 ], [ -0.000295, -0.000098 ], [ -0.000295, -0.000095 ], [ -0.0003, -0.000094 ], [ -0.000309, -0.00009 ], [ -0.000313, -0.000096 ], [ -0.000323, -0.000093 ], [ -0.000327, -0.000088 ], [ -0.000336, -0.000093 ], [ -0.000339, -0.000092 ], [ -0.000339, -0.000083 ], [ -0.000343, -0.000082 ], [ -0.000343, -0.000076 ], [ -0.000342, -0.000074 ], [ -0.000342, -0.000071 ], [ -0.000347, -0.000071 ], [ -0.000345, -0.000058 ], [ -0.000351, -0.000051 ], [ -0.00035, -0.000043 ], [ -0.000354, -0.00004 ], [ -0.000359, -0.000043 ], [ -0.000362, -0.000037 ], [ -0.000368, -0.000038 ], [ -0.000371, -0.000035 ], [ -0.000371, -0.000026 ], [ -0.000374, -0.000025 ], [ -0.000379, -0.000025 ], [ -0.000378, -0.000013 ], [ -0.000385, -0.000011 ], [ -0.000385, -0.0 ], [ -0.000382, 0.000002 ], [ -0.000382, 0.000012 ], [ -0.000389, 0.000012 ], [ -0.000391, 0.000015 ], [ -0.000392, 0.000021 ], [ -0.000383, 0.000033 ], [ -0.000384, 0.000038 ], [ -0.000375, 0.000052 ], [ -0.000368, 0.000054 ], [ -0.000369, 0.00006 ], [ -0.000364, 0.000064 ], [ -0.00036, 0.000064 ], [ -0.000353, 0.000064 ], [ -0.000343, 0.000069 ], [ -0.000338, 0.000069 ], [ -0.000332, 0.00007 ], [ -0.000327, 0.000077 ], [ -0.000327, 0.000084 ], [ -0.000318, 0.000089 ], [ -0.000311, 0.00009 ], [ -0.000306, 0.000094 ], [ -0.000297, 0.000094 ], [ -0.000298, 0.000104 ], [ -0.000289, 0.00011 ], [ -0.000283, 0.000106 ], [ -0.000275, 0.000103 ], [ -0.000276, 0.000116 ], [ -0.000285, 0.000117 ], [ -0.000285, 0.000124 ], [ -0.000278, 0.000128 ], [ -0.000278, 0.000143 ], [ -0.000276, 0.000145 ], [ -0.00027, 0.000146 ], [ -0.000264, 0.00016 ], [ -0.000262, 0.000165 ], [ -0.000252, 0.000162 ], [ -0.000246, 0.000173 ], [ -0.00024, 0.000172 ], [ -0.000229, 0.00018 ], [ -0.000223, 0.00018 ], [ -0.00022, 0.000191 ], [ -0.000215, 0.000193 ], [ -0.000211, 0.000203 ], [ -0.000197, 0.000215 ], [ -0.000195, 0.000222 ], [ -0.000181, 0.000217 ], [ -0.000182, 0.000212 ], [ -0.000188, 0.000202 ], [ -0.000186, 0.000197 ], [ -0.000186, 0.000193 ], [ -0.000188, 0.000183 ], [ -0.000183, 0.000183 ], [ -0.000176, 0.000174 ], [ -0.000172, 0.000174 ], [ -0.000163, 0.000185 ], [ -0.000159, 0.000186 ], [ -0.000154, 0.00019 ], [ -0.000148, 0.000184 ], [ -0.000138, 0.000191 ], [ -0.000126, 0.000189 ], [ -0.000125, 0.000182 ], [ -0.000118, 0.000181 ], [ -0.000117, 0.00019 ], [ -0.000105, 0.000191 ], [ -0.000105, 0.000211 ], [ -0.000108, 0.000218 ], [ -0.000103, 0.00022 ], [ -0.000102, 0.00023 ], [ -0.000091, 0.000235 ], [ -0.000087, 0.000233 ], [ -0.000087, 0.00024 ], [ -0.000083, 0.000245 ], [ -0.000075, 0.000247 ], [ -0.000065, 0.000238 ], [ -0.000059, 0.000237 ], [ -0.000058, 0.000234 ], [ -0.00004, 0.000233 ], [ -0.000039, 0.000241 ], [ -0.00003, 0.000243 ], [ -0.00003, 0.000236 ], [ -0.000026, 0.000236 ], [ -0.000018, 0.000222 ], [ -0.00001, 0.000222 ], [ -0.000007, 0.000217 ], [ -0.000003, 0.000217 ], [ 0.000003, 0.000211 ], [ 0.00001, 0.000215 ], [ 0.000012, 0.000222 ], [ 0.000017, 0.000222 ], [ 0.00003, 0.000233 ], [ 0.000034, 0.000232 ], [ 0.000036, 0.000241 ], [ 0.000065, 0.000239 ], [ 0.000069, 0.000243 ], [ 0.00007, 0.000248 ], [ 0.000064, 0.000248 ], [ 0.000054, 0.000255 ], [ 0.000057, 0.00026 ], [ 0.000057, 0.000275 ], [ 0.00006, 0.000277 ], [ 0.000064, 0.000263 ], [ 0.000069, 0.000262 ], [ 0.000073, 0.000257 ], [ 0.000084, 0.000257 ] ], [ [ -0.000073, -0.000175 ], [ -0.000066, -0.000173 ], [ -0.000043, -0.000167 ], [ -0.000017, -0.000164 ], [ -0.000016, -0.000157 ], [ -0.000057, -0.000157 ], [ -0.000058, -0.000164 ], [ -0.000062, -0.000166 ], [ -0.000066, -0.000164 ], [ -0.000067, -0.000152 ], [ -0.000072, -0.000152 ], [ -0.000072, -0.000157 ], [ -0.000068, -0.00016 ], [ -0.00007, -0.000165 ], [ -0.00007, -0.000171 ], [ -0.000073, -0.000175 ] ], [ [ -0.000007, -0.000157 ], [ -0.000007, -0.000162 ], [ 0.000015, -0.000161 ], [ 0.000037, -0.000162 ], [ 0.000037, -0.000158 ], [ -0.000007, -0.000157 ] ] ] ] } }\n]\n}\n'''
+                return response(200, null_geojson.encode('utf8'), headers={'Content-Type': 'application/json'})
+
+            raise Exception()
+        
+        with HTTMock(response_geojson):
+            calculate.calculate(DATABASE_URL)
+
+        with psycopg2.connect(DATABASE_URL) as conn:
+            with conn.cursor() as db:
+                db.execute('select iso_a2, addr_count, area_total, area_pct, pop_total, pop_pct from areas')
+                (row, ) = db.fetchall()
+                self.assertEqual(row, ('XX', 9999, 2000, 1.0, 20000, 1.0))
diff --git a/test.py b/test.py
index e582c76..2a0b48d 100755
--- a/test.py
+++ b/test.py
@@ -31,6 +31,7 @@ from openaddr.tests.util import TestUtilities
 from openaddr.tests.summarize import TestSummarizeFunctions
 from openaddr.tests.parcels import TestParcelsUtils, TestParcelsParse
 from openaddr.tests.dashboard_stats import TestDashboardStats
+from openaddr.tests.coverage import TestCalculate
 
 from openaddr.tests.ci import (
     TestHook, TestRuns, TestWorker, TestBatch, TestObjects, TestCollect,