address_verify 0.1.1
address_verify: ^0.1.1 copied to clipboard
On-device proof-of-address pre-screening for Flutter. Runs OCR and fuses fuzzy address, name, and GPS signals into a confidence score; not a verification authority.
address_verify #
On-device proof-of-address pre-screening for Flutter. address_verify
captures a proof-of-address document, runs OCR, and produces a confidence
score with a per-signal breakdown. This package is not a verification authority
and produces no guaranteed verification verdict. On-device signals such as GPS
are spoofable; the package surfaces spoof indicators (e.g. mocked-location
flag) but does not claim to prevent spoofing. Use the score and breakdown as
input to your own pre-screening flow.
What this is #
- An on-device OCR + fuzzy-match engine for proof-of-address documents.
- A signal-fusion confidence scorer (address match, name-on-document match, optional GPS country cross-reference).
- A configurable, themeable capture widget — plus a headless engine for bring-your-own-UI.
What this is NOT #
- This package is not a verification authority. The result is a confidence score, not a verdict.
- A fraud-proof system. GPS and on-device readings are spoofable.
- A network service. The core runs fully offline; no API keys, no backend.
Out of scope for the current release: VPN/IP detection, document authenticity/tampering checks, web/desktop OCR, server components.
Install #
flutter pub add address_verify
Platform: iOS + Android. See Platform setup below for required permissions and SDK levels.
Quick start — widget #
import 'package:address_verify/address_verify.dart';
import 'package:flutter/material.dart';
class MyScreen extends StatelessWidget {
const MyScreen({super.key});
@override
Widget build(BuildContext context) {
final config = AddressVerifyConfig(
documentTypes: const [
DocumentType(id: 'utility_bill', label: 'Utility Bill'),
DocumentType(id: 'bank_statement', label: 'Bank Statement'),
],
matchName: true,
detectLocation: false,
);
return Scaffold(
body: AddressVerifyWidget(
config: config,
onComplete: (result) {
debugPrint('confidence=${result.confidence} verdict=${result.verdict}');
},
onError: (error, stack) {
debugPrint('pre-screening failed: $error');
},
),
);
}
}
Quick start — headless engine #
import 'package:address_verify/address_verify.dart';
import 'package:file_picker/file_picker.dart';
Future<void> run(PlatformFile pickedFile) async {
final engine = AddressVerifyEngine(
AddressVerifyConfig(
documentTypes: const [
DocumentType(id: 'utility_bill', label: 'Utility Bill'),
],
matchName: true,
),
);
final result = await engine.verify(
file: pickedFile,
typedAddress: const TypedAddress(
line1: '12 Adeola Odeku Street',
city: 'Lagos',
country: 'NG',
),
documentType: const DocumentType(
id: 'utility_bill',
label: 'Utility Bill',
),
fullName: 'Adaeze Okeke',
);
print('confidence: ${result.confidence}');
print('verdict: ${result.verdict}');
for (final s in result.breakdown.signals) {
print(' ${s.name}: score=${s.score} weight=${s.weight} (${s.detail})');
}
}
The engine also exposes granular helpers (extractText, matchAddress,
matchName) for callers building a custom UI.
Configuration #
AddressVerifyConfig controls the engine and the widget. Every field has a
default; only documentTypes is required.
| Field | Type | Default | Meaning |
|---|---|---|---|
documentTypes |
List<DocumentType> |
required, non-empty | Document types the user may select. |
allowedFormats |
List<FileFormat> |
[pdf, png, jpg] |
Accepted file containers; others are rejected before processing. |
maxFileSizeMb |
int |
10 |
Maximum file size in megabytes; oversize files throw FileTooLargeException. |
returnMode |
ReturnMode |
path |
Whether the captured document is returned as a temp file path or a base64 string. |
detectLocation |
bool |
false |
Opt in to GPS country cross-reference (adds the location signal). |
matchName |
bool |
true |
Score the supplied full name against OCR text (adds the name signal). |
signalWeights |
SignalWeights |
address=0.45 name=0.25 loc=0.30 |
Weights used when fusing present signals. |
matchThreshold |
double |
0.75 |
Confidence at or above this maps to verdict strong. |
ocrLanguage |
String |
'en' |
OCR language hint passed to the provider (mapped to ML Kit script). |
theme |
AddressVerifyTheme |
empty (Material defaults) | Theming for the widget. All visual values resolve to Theme.of(context) when null. |
ocrProvider |
OcrProvider? |
null |
Inject a custom OCR backend. Defaults to MlKitOcrProvider when null. |
Signals and confidence #
The engine fuses up to three independent signals into a single confidence
score in the range 0.0..1.0. Absent signals do not deflate the score —
present weights are re-normalized to sum to 1 before averaging.
present_signals = [address] // always present
if matchName: present_signals += [name]
if detectLocation: present_signals += [location]
total_weight = sum(weight_i for i in present_signals)
confidence = sum(score_i * weight_i for i in present_signals) / total_weight
Per-signal scoring #
- address — token-based fuzzy match between
TypedAddressand OCR text. Combines Jaccard token overlap (weight0.4) with weighted critical-token hits over street number, theline1body, andcity(weight0.6). - name — fraction of
fullNametokens located in OCR text. If no tokens match, the score is0and thenameNotFoundflag is raised. - location — compares the device's GPS-resolved country to
typedAddress.country. Match →1.0; mismatch →0.0plus alocationMismatchflag. When the OS reportsposition.isMocked, the signal is multiplied by0.3and amockedLocationflag is raised.
Normalization rules #
Before matching, both the typed address and OCR text are lowercased,
stripped of .,#-/ punctuation, whitespace-collapsed, and run through a
small bidirectional abbreviation table (street ↔ st, road ↔ rd,
avenue ↔ ave, etc.) so reordered or abbreviated tokens still match.
Verdict thresholds #
confidence >= matchThreshold -> MatchVerdict.strong
confidence >= matchThreshold * 0.66 -> MatchVerdict.partial
otherwise -> MatchVerdict.weak
The ConfidenceBreakdown returned alongside every result lists each present
signal with its raw score, re-normalized weight, and a short detail
string explaining the score.
Platform setup #
address_verify wraps google_mlkit_text_recognition, pdfx,
geolocator, and geocoding. Your host app must declare the matching
permissions and SDK levels before any of them will work.
iOS #
Add the following keys to ios/Runner/Info.plist. Keep the user-facing
strings honest — this is pre-screening, not verification.
<key>NSCameraUsageDescription</key>
<string>Used to capture proof-of-address documents for on-device pre-screening.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Used to read proof-of-address documents for on-device pre-screening.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Used to cross-reference the country on your proof-of-address document for pre-screening.</string>
Set the minimum iOS deployment target to 12.0 (ML Kit requirement) in
ios/Podfile (platform :ios, '12.0') and on the Runner target in Xcode.
Note: Position.isMocked is always reported as false on iOS. The
underlying iOS location APIs do not expose a mock-location flag for
foreground readings, so the mockedLocation flag never fires on iOS.
Android #
Add the location permissions to
android/app/src/main/AndroidManifest.xml inside the <manifest> element:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
Set minSdkVersion (or minSdk in build.gradle.kts) to 21 or higher
in android/app/build.gradle/build.gradle.kts. ML Kit text recognition
requires API 21+.
defaultConfig {
minSdk = 21
}
Runtime permission prompts are handled by geolocator the first time
detectLocation: true is used.
Geocoding caveats #
geocoding performs reverse geocoding via the platform's built-in service
(CLGeocoder on iOS, Geocoder on Android). Both providers may require a
network call on first use; offline behaviour is undefined. Both are
rate-limited by the OS — the engine treats failures and empty results as
"country unknown" rather than throwing, so the location signal degrades
gracefully into a locationMismatch flag.
ML Kit caveats #
- The first OCR pass downloads the on-device model lazily. On low-storage
devices this can fail; the engine raises
OcrUnavailableExceptionin that case. meanConfidenceonOcrResultis populated on Android only. iOS ML Kit does not expose per-line confidence, so the field isnulland thelowOcrConfidenceflag never fires on iOS.
Bring-your-own OCR #
The default OCR backend is MlKitOcrProvider. To swap it (for tests, a
server-backed provider, or a non-Latin script), implement OcrProvider and
pass it via config.ocrProvider:
import 'dart:typed_data';
import 'package:address_verify/address_verify.dart';
class MyOcrProvider implements OcrProvider {
@override
Future<OcrResult> extract(Uint8List imageBytes) async {
final text = await myBackend.run(imageBytes);
return OcrResult(text: text, meanConfidence: null);
}
}
final config = AddressVerifyConfig(
documentTypes: const [DocumentType(id: 'utility_bill', label: 'Utility Bill')],
ocrProvider: MyOcrProvider(),
);
Limitations and known caveats #
- GPS is spoofable. The OS mock-location flag is surfaced as a
mockedLocationadvisory but is not a guarantee against rooted devices, custom ROMs, or developer-mode tooling. - Geocoding is OS-dependent. On both iOS and Android the first reverse geocode may require a network round-trip and is rate-limited by the platform. Failures degrade to "country unknown".
- OCR quality varies by document. Scans, photos at acute angles, low
contrast, glossy laminates, and non-Latin scripts all reduce accuracy.
The default ML Kit configuration is tuned for the Latin script; pass
ocrLanguageor inject your ownOcrProviderfor other scripts. - PDF rasterization uses
pdfx. The engine renders page 1, and falls back through up to three pages when page 1 yields less than 20 characters of OCR text. - iOS mocked-location is never flagged (see above).
License #
MIT — see LICENSE.