Privacy-Preserving Record Linkage
Overview
Privacy-Preserving Record Linkage (PPRL) enables two or more organizations to identify matching records across their datasets without revealing the underlying personal data to each other. This is critical for healthcare research, fraud detection, national statistics, and cross-organizational analytics where direct data sharing is prohibited by privacy regulations.
Approach Comparison
| Approach |
Privacy Level |
Accuracy |
Scalability |
Communication Cost |
| Bloom Filter Encoding |
High |
Good (>95% F1) |
Very High |
Low |
| Secure Hash Matching |
Very High |
High (exact match only) |
Very High |
Very Low |
| Secure Multi-Party Computation |
Cryptographic |
Very High |
Medium |
High |
| Trusted Third Party |
Depends on TTP |
Very High |
High |
Medium |
| Differential Privacy Linkage |
Formally private |
Moderate |
High |
Low |
Bloom Filter-Based PPRL
How It Works
- Each organization encodes their quasi-identifiers (name, date of birth, address) into Bloom filters
- The Bloom filter encoding uses cryptographic hash functions with a shared secret key
- Encoded Bloom filters are compared using similarity metrics (Dice coefficient, Jaccard)
- Matching pairs above a threshold are identified as linked records
- Raw data is never exchanged — only Bloom filter bit arrays
Bloom Filter Encoding Implementation
"""
Privacy-preserving record linkage using Bloom filter encoding.
Implements the approach described by Schnell, Bachteler, and Reiher (2009).
"""
import hashlib
import hmac
import math
from typing import Optional
import numpy as np
class BloomFilterEncoder:
"""
Encode string attributes into Bloom filters for privacy-preserving
record linkage using cryptographic keyed hashing.
"""
def __init__(
self,
filter_size: int = 1024,
num_hash_functions: int = 30,
ngram_size: int = 2,
secret_key: bytes = b""
):
"""
Args:
filter_size: Number of bits in the Bloom filter
num_hash_functions: Number of hash functions (k)
ngram_size: Size of character n-grams (bigrams = 2)
secret_key: Shared secret key for HMAC hashing
"""
self.filter_size = filter_size
self.num_hash_functions = num_hash_functions
self.ngram_size = ngram_size
self.secret_key = secret_key
def _generate_ngrams(self, value: str) -> list[str]:
"""Generate character n-grams from a string value."""
# Pad the string to handle edge characters
padded = f"_{value}_"
return [
padded[i:i + self.ngram_size]
for i in range(len(padded) - self.ngram_size + 1)
]
def _hash_ngram(self, ngram: str, hash_index: int) -> int:
"""
Hash an n-gram using HMAC with the shared key and hash index.
Returns a bit position in the Bloom filter.
"""
message = f"{hash_index}:{ngram}".encode("utf-8")
digest = hmac.new(self.secret_key, message, hashlib.sha256).digest()
# Convert first 4 bytes to integer and modulo by filter size
position = int.from_bytes(digest[:4], byteorder="big") % self.filter_size
return position
def encode_value(self, value: str) -> np.ndarray:
"""
Encode a single attribute value into a Bloom filter.
Args:
value: The string value to encode (e.g., a name)
Returns:
Numpy array of bits (0/1) representing the Bloom filter
"""
bloom_filter = np.zeros(self.filter_size, dtype=np.uint8)
# Normalize the input
normalized = value.strip().lower()
# Generate n-grams
ngrams = self._generate_ngrams(normalized)
# Hash each n-gram with each hash function
for ngram in ngrams:
for h in range(self.num_hash_functions):
position = self._hash_ngram(ngram, h)
bloom_filter[position] = 1
return bloom_filter
def encode_record(self, attributes: dict[str, str]) -> np.ndarray:
"""
Encode multiple attributes into a single composite Bloom filter
using Cryptographic Longterm Key (CLK) approach.
Args:
attributes: Dict mapping attribute names to values
e.g., {"first_name": "John", "last_name": "Smith", "dob": "1990-01-15"}
Returns:
Composite Bloom filter as numpy array
"""
composite = np.zeros(self.filter_size, dtype=np.uint8)
for attr_name, attr_value in attributes.items():
if attr_value:
# Use attribute name as additional salt
salted_key = self.secret_key + attr_name.encode("utf-8")
encoder = BloomFilterEncoder(
filter_size=self.filter_size,
num_hash_functions=self.num_hash_functions,
ngram_size=self.ngram_size,
secret_key=salted_key
)
attr_bf = encoder.encode_value(attr_value)
composite = np.bitwise_or(composite, attr_bf)
return composite
class BloomFilterMatcher:
"""
Compare Bloom filter-encoded records to find matching pairs.
"""
@staticmethod
def dice_coefficient(bf1: np.ndarray, bf2: np.ndarray) -> float:
"""
Calculate the Dice coefficient between two Bloom filters.
Dice = 2 * |bf1 AND bf2| / (|bf1| + |bf2|)
Returns a similarity score between 0 and 1.
"""
intersection = np.sum(np.bitwise_and(bf1, bf2))
cardinality_sum = np.sum(bf1) + np.sum(bf2)
if cardinality_sum == 0:
return 0.0
return 2.0 * intersection / cardinality_sum
@staticmethod
def jaccard_similarity(bf1: np.ndarray, bf2: np.ndarray) -> float:
"""
Calculate the Jaccard similarity between two Bloom filters.
Jaccard = |bf1 AND bf2| / |bf1 OR bf2|
"""
intersection = np.sum(np.bitwise_and(bf1, bf2))
union = np.sum(np.bitwise_or(bf1, bf2))
if union == 0:
return 0.0
return intersection / union
def find_matches(
self,
encodings_a: list[tuple[str, np.ndarray]],
encodings_b: list[tuple[str, np.ndarray]],
threshold: float = 0.8,
similarity_metric: str = "dice"
) -> list[tuple[str, str, float]]:
"""
Find matching record pairs between two encoded datasets.
Args:
encodings_a: List of (record_id, bloom_filter) from organization A
encodings_b: List of (record_id, bloom_filter) from organization B
threshold: Minimum similarity score for a match
similarity_metric: "dice" or "jaccard"
Returns:
List of (id_a, id_b, similarity_score) for matching pairs
"""
metric_fn = (
self.dice_coefficient if similarity_metric == "dice"
else self.jaccard_similarity
)
matches = []
for id_a, bf_a in encodings_a:
best_score = 0.0
best_id_b = None
for id_b, bf_b in encodings_b:
score = metric_fn(bf_a, bf_b)
if score > best_score:
best_score = score
best_id_b = id_b
if best_score >= threshold and best_id_b is not None:
matches.append((id_a, best_id_b, best_score))
return matches
Secure Hash Matching
For exact matching scenarios where approximate matching is not needed.
"""
Secure hash-based record linkage for exact matching.
Uses keyed HMAC to prevent rainbow table attacks.
"""
import hashlib
import hmac
class SecureHashLinker:
"""
Link records across organizations using keyed hash matching.
Suitable for exact match on standardized identifiers.
"""
def __init__(self, shared_key: bytes):
self.shared_key = shared_key
def hash_identifier(self, *fields: str) -> str:
"""
Create a keyed hash of concatenated identifier fields.
Args:
fields: Identifier fields in standardized order
e.g., ("john", "smith", "19900115")
Returns:
Hex-encoded HMAC-SHA256 hash
"""
# Normalize and concatenate fields
normalized = "|".join(f.strip().lower() for f in fields)
# Generate keyed hash
digest = hmac.new(
self.shared_key,
normalized.encode("utf-8"),
hashlib.sha256
).hexdigest()
return digest
def hash_dataset(
self,
records: list[dict],
id_field: str,
linkage_fields: list[str]
) -> dict[str, str]:
"""
Hash all records in a dataset for linkage.
Returns mapping of hash -> record_id.
"""
hash_map = {}
for record in records:
fields = [str(record.get(f, "")) for f in linkage_fields]
record_hash = self.hash_identifier(*fields)
hash_map[record_hash] = record[id_field]
return hash_map
@staticmethod
def find_exact_matches(
hashes_a: dict[str, str],
hashes_b: dict[str, str]
) -> list[tuple[str, str]]:
"""
Find exact matches between two hash maps.
Returns list of (id_a, id_b) pairs.
"""
common_hashes = set(hashes_a.keys()) & set(hashes_b.keys())
return [(hashes_a[h], hashes_b[h]) for h in common_hashes]
Threshold Tuning
Methodology
| Threshold Range |
Precision |
Recall |
Use Case |
| 0.90 - 1.00 |
Very High |
Low |
High-stakes decisions (medical records) |
| 0.80 - 0.90 |
High |
Medium |
Standard record linkage |
| 0.70 - 0.80 |
Medium |
High |
Exploratory analysis, broad matching |
| 0.60 - 0.70 |
Low |
Very High |
Candidate generation (with manual review) |
Optimal Threshold Selection Process
- Generate labeled pairs: Create a sample of known matches and non-matches
- Compute similarity scores: Calculate Dice/Jaccard for all pairs in the sample
- Plot precision-recall curve: Sweep threshold from 0 to 1
- Select threshold: Choose based on acceptable false positive rate for the use case
- Validate: Test on held-out labeled data
False Positive Management
"""
Post-linkage false positive reduction through multi-stage verification.
"""
class FalsePositiveManager:
"""
Reduce false positive matches through additional verification stages
without revealing raw data between parties.
"""
def __init__(self, primary_threshold: float = 0.8, verification_threshold: float = 0.9):
self.primary_threshold = primary_threshold
self.verification_threshold = verification_threshold
def multi_field_verification(
self,
candidate_pairs: list[tuple[str, str, float]],
secondary_encodings_a: dict[str, dict[str, np.ndarray]],
secondary_encodings_b: dict[str, dict[str, np.ndarray]],
matcher: BloomFilterMatcher
) -> list[tuple[str, str, float, bool]]:
"""
Verify candidate matches using additional encoded fields.
Args:
candidate_pairs: (id_a, id_b, primary_score) from initial matching
secondary_encodings_a: {record_id: {field: bloom_filter}} from org A
secondary_encodings_b: {record_id: {field: bloom_filter}} from org B
Returns:
(id_a, id_b, composite_score, verified) for each candidate
"""
verified_pairs = []
for id_a, id_b, primary_score in candidate_pairs:
secondary_scores = []
fields_a = secondary_encodings_a.get(id_a, {})
fields_b = secondary_encodings_b.get(id_b, {})
common_fields = set(fields_a.keys()) & set(fields_b.keys())
for field_name in common_fields:
score = matcher.dice_coefficient(
fields_a[field_name],
fields_b[field_name]
)
secondary_scores.append(score)
if secondary_scores:
avg_secondary = sum(secondary_scores) / len(secondary_scores)
composite = 0.6 * primary_score + 0.4 * avg_secondary
verified = composite >= self.verification_threshold
else:
composite = primary_score
verified = primary_score >= self.verification_threshold
verified_pairs.append((id_a, id_b, composite, verified))
return verified_pairs
Security Considerations
| Attack |
Description |
Mitigation |
| Frequency analysis |
Analyzing bit patterns to infer common values |
Use composite Bloom filters (CLK), add noise bits |
| Dictionary attack |
Pre-computing Bloom filters for known values |
Use strong shared secret keys, rotate keys periodically |
| Bit pattern cryptanalysis |
Exploiting structure in Bloom filter bit patterns |
Sufficient filter size (>= 1024), adequate hash functions (>= 20) |
| Collision exploitation |
Deliberately crafting records to match target hashes |
HMAC-based hashing, input validation |
References
- Schnell, R., Bachteler, T., and Reiher, J. "Privacy-Preserving Record Linkage Using Bloom Filters." BMC Medical Informatics and Decision Making, 9(1):41, 2009.
- Vatsalan, D., Christen, P., and Verykios, V.S. "A Taxonomy of Privacy-Preserving Record Linkage Techniques." Information Systems, 38(6):946-969, 2013.
- Randall, S.M. et al. "Privacy-Preserving Record Linkage on Large Real World Datasets." Journal of Biomedical Informatics, 50:205-212, 2014.
- AIHW (Australian Institute of Health and Welfare) PPRL Implementation Guide
- Christen, P. "Data Matching: Concepts and Techniques for Record Linkage, Entity Resolution, and Duplicate Detection." Springer, 2012.
1---2name: privacy-record-linkage3description: Implement privacy-preserving record linkage across datasets using Bloom filter encoding, secure hash matching, threshold tuning for precision and recall, and false positive management. Enables entity resolution without exposing raw personally identifiable information between parties.4license: Apache-2.05---67# Privacy-Preserving Record Linkage89## Overview1011Privacy-Preserving Record Linkage (PPRL) enables two or more organizations to identify matching records across their datasets without revealing the underlying personal data to each other. This is critical for healthcare research, fraud detection, national statistics, and cross-organizational analytics where direct data sharing is prohibited by privacy regulations.1213## Approach Comparison1415| Approach | Privacy Level | Accuracy | Scalability | Communication Cost |16|----------|--------------|----------|-------------|-------------------|17| Bloom Filter Encoding | High | Good (>95% F1) | Very High | Low |18| Secure Hash Matching | Very High | High (exact match only) | Very High | Very Low |19| Secure Multi-Party Computation | Cryptographic | Very High | Medium | High |20| Trusted Third Party | Depends on TTP | Very High | High | Medium |21| Differential Privacy Linkage | Formally private | Moderate | High | Low |2223## Bloom Filter-Based PPRL2425### How It Works26271. Each organization encodes their quasi-identifiers (name, date of birth, address) into Bloom filters282. The Bloom filter encoding uses cryptographic hash functions with a shared secret key293. Encoded Bloom filters are compared using similarity metrics (Dice coefficient, Jaccard)304. Matching pairs above a threshold are identified as linked records315. Raw data is never exchanged — only Bloom filter bit arrays3233### Bloom Filter Encoding Implementation3435```python36"""37Privacy-preserving record linkage using Bloom filter encoding.38Implements the approach described by Schnell, Bachteler, and Reiher (2009).39"""4041import hashlib42import hmac43import math44from typing import Optional45import numpy as np464748class BloomFilterEncoder:49 """50 Encode string attributes into Bloom filters for privacy-preserving51 record linkage using cryptographic keyed hashing.52 """5354 def __init__(55 self,56 filter_size: int = 1024,57 num_hash_functions: int = 30,58 ngram_size: int = 2,59 secret_key: bytes = b""60 ):61 """62 Args:63 filter_size: Number of bits in the Bloom filter64 num_hash_functions: Number of hash functions (k)65 ngram_size: Size of character n-grams (bigrams = 2)66 secret_key: Shared secret key for HMAC hashing67 """68 self.filter_size = filter_size69 self.num_hash_functions = num_hash_functions70 self.ngram_size = ngram_size71 self.secret_key = secret_key7273 def _generate_ngrams(self, value: str) -> list[str]:74 """Generate character n-grams from a string value."""75 # Pad the string to handle edge characters76 padded = f"_{value}_"77 return [78 padded[i:i + self.ngram_size]79 for i in range(len(padded) - self.ngram_size + 1)80 ]8182 def _hash_ngram(self, ngram: str, hash_index: int) -> int:83 """84 Hash an n-gram using HMAC with the shared key and hash index.85 Returns a bit position in the Bloom filter.86 """87 message = f"{hash_index}:{ngram}".encode("utf-8")88 digest = hmac.new(self.secret_key, message, hashlib.sha256).digest()89 # Convert first 4 bytes to integer and modulo by filter size90 position = int.from_bytes(digest[:4], byteorder="big") % self.filter_size91 return position9293 def encode_value(self, value: str) -> np.ndarray:94 """95 Encode a single attribute value into a Bloom filter.9697 Args:98 value: The string value to encode (e.g., a name)99100 Returns:101 Numpy array of bits (0/1) representing the Bloom filter102 """103 bloom_filter = np.zeros(self.filter_size, dtype=np.uint8)104105 # Normalize the input106 normalized = value.strip().lower()107108 # Generate n-grams109 ngrams = self._generate_ngrams(normalized)110111 # Hash each n-gram with each hash function112 for ngram in ngrams:113 for h in range(self.num_hash_functions):114 position = self._hash_ngram(ngram, h)115 bloom_filter[position] = 1116117 return bloom_filter118119 def encode_record(self, attributes: dict[str, str]) -> np.ndarray:120 """121 Encode multiple attributes into a single composite Bloom filter122 using Cryptographic Longterm Key (CLK) approach.123124 Args:125 attributes: Dict mapping attribute names to values126 e.g., {"first_name": "John", "last_name": "Smith", "dob": "1990-01-15"}127128 Returns:129 Composite Bloom filter as numpy array130 """131 composite = np.zeros(self.filter_size, dtype=np.uint8)132133 for attr_name, attr_value in attributes.items():134 if attr_value:135 # Use attribute name as additional salt136 salted_key = self.secret_key + attr_name.encode("utf-8")137 encoder = BloomFilterEncoder(138 filter_size=self.filter_size,139 num_hash_functions=self.num_hash_functions,140 ngram_size=self.ngram_size,141 secret_key=salted_key142 )143 attr_bf = encoder.encode_value(attr_value)144 composite = np.bitwise_or(composite, attr_bf)145146 return composite147148149class BloomFilterMatcher:150 """151 Compare Bloom filter-encoded records to find matching pairs.152 """153154 @staticmethod155 def dice_coefficient(bf1: np.ndarray, bf2: np.ndarray) -> float:156 """157 Calculate the Dice coefficient between two Bloom filters.158159 Dice = 2 * |bf1 AND bf2| / (|bf1| + |bf2|)160161 Returns a similarity score between 0 and 1.162 """163 intersection = np.sum(np.bitwise_and(bf1, bf2))164 cardinality_sum = np.sum(bf1) + np.sum(bf2)165166 if cardinality_sum == 0:167 return 0.0168169 return 2.0 * intersection / cardinality_sum170171 @staticmethod172 def jaccard_similarity(bf1: np.ndarray, bf2: np.ndarray) -> float:173 """174 Calculate the Jaccard similarity between two Bloom filters.175176 Jaccard = |bf1 AND bf2| / |bf1 OR bf2|177 """178 intersection = np.sum(np.bitwise_and(bf1, bf2))179 union = np.sum(np.bitwise_or(bf1, bf2))180181 if union == 0:182 return 0.0183184 return intersection / union185186 def find_matches(187 self,188 encodings_a: list[tuple[str, np.ndarray]],189 encodings_b: list[tuple[str, np.ndarray]],190 threshold: float = 0.8,191 similarity_metric: str = "dice"192 ) -> list[tuple[str, str, float]]:193 """194 Find matching record pairs between two encoded datasets.195196 Args:197 encodings_a: List of (record_id, bloom_filter) from organization A198 encodings_b: List of (record_id, bloom_filter) from organization B199 threshold: Minimum similarity score for a match200 similarity_metric: "dice" or "jaccard"201202 Returns:203 List of (id_a, id_b, similarity_score) for matching pairs204 """205 metric_fn = (206 self.dice_coefficient if similarity_metric == "dice"207 else self.jaccard_similarity208 )209210 matches = []211212 for id_a, bf_a in encodings_a:213 best_score = 0.0214 best_id_b = None215216 for id_b, bf_b in encodings_b:217 score = metric_fn(bf_a, bf_b)218 if score > best_score:219 best_score = score220 best_id_b = id_b221222 if best_score >= threshold and best_id_b is not None:223 matches.append((id_a, best_id_b, best_score))224225 return matches226```227228## Secure Hash Matching229230For exact matching scenarios where approximate matching is not needed.231232```python233"""234Secure hash-based record linkage for exact matching.235Uses keyed HMAC to prevent rainbow table attacks.236"""237238import hashlib239import hmac240241242class SecureHashLinker:243 """244 Link records across organizations using keyed hash matching.245 Suitable for exact match on standardized identifiers.246 """247248 def __init__(self, shared_key: bytes):249 self.shared_key = shared_key250251 def hash_identifier(self, *fields: str) -> str:252 """253 Create a keyed hash of concatenated identifier fields.254255 Args:256 fields: Identifier fields in standardized order257 e.g., ("john", "smith", "19900115")258259 Returns:260 Hex-encoded HMAC-SHA256 hash261 """262 # Normalize and concatenate fields263 normalized = "|".join(f.strip().lower() for f in fields)264265 # Generate keyed hash266 digest = hmac.new(267 self.shared_key,268 normalized.encode("utf-8"),269 hashlib.sha256270 ).hexdigest()271272 return digest273274 def hash_dataset(275 self,276 records: list[dict],277 id_field: str,278 linkage_fields: list[str]279 ) -> dict[str, str]:280 """281 Hash all records in a dataset for linkage.282283 Returns mapping of hash -> record_id.284 """285 hash_map = {}286287 for record in records:288 fields = [str(record.get(f, "")) for f in linkage_fields]289 record_hash = self.hash_identifier(*fields)290 hash_map[record_hash] = record[id_field]291292 return hash_map293294 @staticmethod295 def find_exact_matches(296 hashes_a: dict[str, str],297 hashes_b: dict[str, str]298 ) -> list[tuple[str, str]]:299 """300 Find exact matches between two hash maps.301302 Returns list of (id_a, id_b) pairs.303 """304 common_hashes = set(hashes_a.keys()) & set(hashes_b.keys())305 return [(hashes_a[h], hashes_b[h]) for h in common_hashes]306```307308## Threshold Tuning309310### Methodology311312| Threshold Range | Precision | Recall | Use Case |313|----------------|-----------|--------|----------|314| 0.90 - 1.00 | Very High | Low | High-stakes decisions (medical records) |315| 0.80 - 0.90 | High | Medium | Standard record linkage |316| 0.70 - 0.80 | Medium | High | Exploratory analysis, broad matching |317| 0.60 - 0.70 | Low | Very High | Candidate generation (with manual review) |318319### Optimal Threshold Selection Process3203211. **Generate labeled pairs**: Create a sample of known matches and non-matches3222. **Compute similarity scores**: Calculate Dice/Jaccard for all pairs in the sample3233. **Plot precision-recall curve**: Sweep threshold from 0 to 13244. **Select threshold**: Choose based on acceptable false positive rate for the use case3255. **Validate**: Test on held-out labeled data326327### False Positive Management328329```python330"""331Post-linkage false positive reduction through multi-stage verification.332"""333334335class FalsePositiveManager:336 """337 Reduce false positive matches through additional verification stages338 without revealing raw data between parties.339 """340341 def __init__(self, primary_threshold: float = 0.8, verification_threshold: float = 0.9):342 self.primary_threshold = primary_threshold343 self.verification_threshold = verification_threshold344345 def multi_field_verification(346 self,347 candidate_pairs: list[tuple[str, str, float]],348 secondary_encodings_a: dict[str, dict[str, np.ndarray]],349 secondary_encodings_b: dict[str, dict[str, np.ndarray]],350 matcher: BloomFilterMatcher351 ) -> list[tuple[str, str, float, bool]]:352 """353 Verify candidate matches using additional encoded fields.354355 Args:356 candidate_pairs: (id_a, id_b, primary_score) from initial matching357 secondary_encodings_a: {record_id: {field: bloom_filter}} from org A358 secondary_encodings_b: {record_id: {field: bloom_filter}} from org B359360 Returns:361 (id_a, id_b, composite_score, verified) for each candidate362 """363 verified_pairs = []364365 for id_a, id_b, primary_score in candidate_pairs:366 secondary_scores = []367368 fields_a = secondary_encodings_a.get(id_a, {})369 fields_b = secondary_encodings_b.get(id_b, {})370371 common_fields = set(fields_a.keys()) & set(fields_b.keys())372373 for field_name in common_fields:374 score = matcher.dice_coefficient(375 fields_a[field_name],376 fields_b[field_name]377 )378 secondary_scores.append(score)379380 if secondary_scores:381 avg_secondary = sum(secondary_scores) / len(secondary_scores)382 composite = 0.6 * primary_score + 0.4 * avg_secondary383 verified = composite >= self.verification_threshold384 else:385 composite = primary_score386 verified = primary_score >= self.verification_threshold387388 verified_pairs.append((id_a, id_b, composite, verified))389390 return verified_pairs391```392393## Security Considerations394395| Attack | Description | Mitigation |396|--------|-------------|------------|397| Frequency analysis | Analyzing bit patterns to infer common values | Use composite Bloom filters (CLK), add noise bits |398| Dictionary attack | Pre-computing Bloom filters for known values | Use strong shared secret keys, rotate keys periodically |399| Bit pattern cryptanalysis | Exploiting structure in Bloom filter bit patterns | Sufficient filter size (>= 1024), adequate hash functions (>= 20) |400| Collision exploitation | Deliberately crafting records to match target hashes | HMAC-based hashing, input validation |401402## References403404- Schnell, R., Bachteler, T., and Reiher, J. "Privacy-Preserving Record Linkage Using Bloom Filters." BMC Medical Informatics and Decision Making, 9(1):41, 2009.405- Vatsalan, D., Christen, P., and Verykios, V.S. "A Taxonomy of Privacy-Preserving Record Linkage Techniques." Information Systems, 38(6):946-969, 2013.406- Randall, S.M. et al. "Privacy-Preserving Record Linkage on Large Real World Datasets." Journal of Biomedical Informatics, 50:205-212, 2014.407- AIHW (Australian Institute of Health and Welfare) PPRL Implementation Guide408- Christen, P. "Data Matching: Concepts and Techniques for Record Linkage, Entity Resolution, and Duplicate Detection." Springer, 2012.